C现代方法(第12章)笔记——指针和数组

文章目录

  • 第12章 指针和数组
    • 12.1 指针的算术运算
      • 12.1.1 指针加上整数
      • 12.1.2 指针减去整数
      • 12.1.3 两个指针相减
      • 12.1.4 指针比较
      • 12.1.5 指向复合字面量的指针(C99)
    • 12.2 指针用于数组处理
      • 12.2.1 `*`运算符和`++`运算符的组合
    • 12.3 用数组名作为指针
      • 12.3.1 数组型实际参数(改进版)
      • 12.3.2 用指针作为数组名
    • 12.4 指针和多维数组
      • 12.4.1 处理多维数组的元素
      • 12.4.2 处理多维数组的行
      • 12.4.3 处理多维数组的列
      • 12.4.4 用多维数组名作为指针
    • 12.5 C99中的指针和变长数组
    • 问与答
    • 写在最后

第12章 指针和数组

第11章介绍了指针,并且说明了如何把指针用作函数的实际参数和函数的返回值。本章介绍指针的另一种应用。当指针指向数组元素时,C语言允许对指针进行算术运算(加法和减法),通过这种运算我们可以用指针代替数组下标对数组进行处理。

正如本章将介绍的那样,C语言中指针和数组的关系是非常紧密的。后面的第13章(字符串)和第17章(指针的高级应用)将利用这种关系。理解指针和数组之间的关系对于熟练掌握C语言非常关键:它能使我们深入了解C语言的设计过程,并且能够帮助我们理解现有的程序。然而,需要知道的是,用指针处理数组的主要原因是效率,但是这里的效率提升已经不再像当初那么重要了,这主要归功于编译器的改进。

12.1节讨论指针的算术运算,并且说明如何使用关系运算符和判等运算符进行指针的比较;12.2节示范如何用指针处理数组元素;12.3节揭示了一个关于数组的重要事实(即可以用数组的名字作为指向数组中第一个元素的指针),并且利用这个事实说明了数组型实际参数的真实工作机制;12.4节讲解前3节的主题对于多维数组的应用;最后的12.5节介绍指针和变长数组之间的关系(C99的特性)。

12.1 指针的算术运算

11.5节可知,指针可以指向数组元素。例如,假设已经声明ap如下:

int a[10], *p;

通过下列写法可以使p指向a[0]:

p = &a[0];

现在可以通过p访问a[0]。例如,可以通过下列写法把值5存入a[0]中:

*p = 5;

把指针p指向数组a的元素不是特别令人激动。但是,通过在p上执行指针算术运算(或者地址算术运算)可以访问数组a的其他所有元素。C语言支持3种(而且只有3种)格式的指针算术运算

  • 指针加上整数;
  • 指针减去整数;
  • 两个指针相减。

接下来仔细研究一下每种运算。下面的所有例子都假设有如下声明:

int a[10], *p, *q, i;

12.1.1 指针加上整数

指针p加上整数j产生指向特定元素的指针,这个特定元素是p原先指向的元素后的j个位置。更确切地说,如果p指向数组元素a[i],那么p + j指向a[i + j](当然,前提是a[i + j]必须存在)。

下面的示例说明指针的加法运算:

p = &a[2];  //p指向a数组的第3个元素q = p + 3;  //q指向a数组的第6个元素p += 6;     //p指向a数组的第9个元素

12.1.2 指针减去整数

如果p指向数组元素a[i],那么p - j指向a[i - j]。例如:

p = &a[8];      //p指向a数组第9个元素q = p - 3;      //q指向a数组第6个元素p -= 6;         //p指向a数组第3个元素

12.1.3 两个指针相减

当两个指针相减时,结果为指针之间的距离(用数组元素的个数来度量)。因此,如果p指向a[i]q指向a[j],那么p - q就等于i - j。例如:

p = &a[5];
q = &a[1];i = p - q;   /* i is 4 */ 
i = q - p;   /* i is -4 */ 

请注意!!在一个不指向任何数组元素的指针上执行算术运算会导致未定义的行为。此外,只有在两个指针指向同一个数组时,把它们相减才有意义。

12.1.4 指针比较

可以用关系运算符(<、<=、>和>=)判等运算符(==和!=)进行指针比较只有在两个指针指向同一数组时,用关系运算符进行的指针比较才有意义。比较的结果依赖于数组中两个元素的相对位置。例如,在下面的赋值后p <= q的值是0,而p >= q的值是1

p = &a[5]; 
q = &a[1];

12.1.5 指向复合字面量的指针(C99)

指针指向由复合字面量(9.3节)创建的数组中的某个元素是合法的。回顾一下,复合字面量是C99的一个特性,可以用于创建没有名称的数组。考虑下面的例子:

int *p = (int []){3, 0, 3, 4, 1}; 

p指向一个5元素数组的第一个元素,这个数组包括5个整数:3、0、3、4和1。使用复合字面量可以减少一些麻烦,我们不再需要先声明一个数组变量,然后用指针p指向数组的第一个元素:

int a[] = {3, 0, 3, 4, 1}; 
int *p = &a[0]; 

12.2 指针用于数组处理

指针的算术运算允许通过对指针变量进行重复自增来访问数组的元素。下面这个对数组a中元素求和的程序片段说明了这种方法。在这个示例中,指针变量p初始指向a[0],每次执行循环时对p进行自增。因此p先指向a[1],然后指向a[2],以此类推。在p指向数组a的最后一个元素,后循环终止。

#define N 10 
... 
int a[N], sum, *p; 
... 
sum = 0; 
for (p = &a[0]; p < &a[N]; p++) sum += *p; 

for语句中的条件p < &a[N]值得特别说明一下。尽管元素a[N]不存在(数组a的下标为0~N-1),但是对它使用取地址运算符是合法的。因为循环不会尝试检查a[N]的值,所以在上述方式下使用a[N]是非常安全的。执行循环体时p依次等于&a[0], &a[1], …, &a[N-1],但是当p等于&a[N]时,循环终止。

当然,改用下标可以很容易地写出不使用指针的循环。支持采用指针算术运算的最常见论调是,这样做可以节省执行时间。但是,这依赖于具体的实现——对有些编译器来说,实际上依靠下标的循环会产生更好的代码。

12.2.1 *运算符和++运算符的组合

C程序员经常在处理数组元素的语句中组合*(间接寻址)运算符和++运算符。思考一个简单的例子:把值存入一个数组元素中,然后前进到下一个元素。利用数组下标可以这样写:

a[i++] = j; 

如果p指向数组元素,那么相应的语句将是:

*p++ = j;

因为后缀++的优先级高于*,所以编译器把上述语句看作:

*(p++) = j;

p++的值是p。(因为使用后缀++,所以p只有在表达式计算出来后才可以自增。)因此,*(p++)的值将是*p,即p当前指向的对象。

当然,*p++不是唯一合法的*++的组合。例如,可以编写(*p)++,这个表达式返回p指向的对象,然后对该对象进行自增(p本身是不变化的)。如果觉得困惑,请看下面列举的表达式:

表达式含义
*p++*(p++)自增前表达式的值是*p,以后再自增p
(*p)++自增前表达式的值是*p,以后再自增*p
*++p*(++p)先自增p,自增后表达式的值是*p
++*p++(*p)先自增*p,自增后表达式的值是*p

4种组合都可以出现在程序中,但有些组合比其他组合要常见得多。最频繁见到的就是*p++(或者*(p++)),它在循环中是很方便的。对数组a的元素求和时,可以这样做:

for (p = &a[0]; p < &a[N]; p++) sum += *p;//改写成p = &a[0];  
while (p < &a[N]) sum += *p++;

*运算符--运算符的组合方法类似于*++的组合。

为了应用*--的组合,一起回到10.2节的例子。原始版本的栈依靠名为top的整型变量来记录contents数组中“栈顶”的位置。现在用一个指针变量来替换top,这个指针变量初始指向contents数组的第0个元素。

int *top_ptr = &contents[0];//下面是新的push函数和pop函数(把更新其他栈函数留作练习):void push(int i) 
{ if (is_full()) stack_overflow(); else *top_ptr++ = i; 
} int pop(void) 
{ if (is_empty()) stack_underflow(); elsereturn *--top_ptr; 
}

注意!!因为希望pop函数在取回top_ptr指向的值之前对top_ptr进行自减,所以要写成*--top_ptr,而不是*top_ptr--

12.3 用数组名作为指针

指针的算术运算是数组和指针之间相互关联的一种方法,但这不是两者之间唯一的联系。下面是另一种关键的关系:可以用数组的名字作为指向数组第一个元素的指针。这种关系简化了指针的算术运算,而且使数组和指针更加通用。

例如,假设用如下形式声明a

int a[10];

a作为指向数组第一个元素的指针,可以修改a[0]

*a = 7;     /* stores 7 in a[0] */ 

可以通过指针a + 1来修改a[1]

*(a+1) = 12; /* store 12 in a[1] */

通常情况下,a + i等同于&a[i](两者都表示指向数组a中元素i的指针),并且*(a+i)等价于a[i](两者都表示元素i本身)。换句话说,可以把数组的取下标操作看作指针算术运算的一种形式。

数组名可以用作指针这一事实使得编写遍历数组的循环更加容易。思考下面这个来自12.2节的循环:

for (p = &a[0]; p < &a[N]; p++) sum += *p;

为了简化这个循环,可以用a替换&a[0],同时用a + N替换&a[N]

for (p = a; p < a + N; p++) sum += *p; 

请注意!!虽然可以把数组名用作指针,但是不能给数组名赋新的值。试图使数组名指向其他地方是错误的:

while (*a != 0) a++;    /* wrong */

这一限制不会对我们造成什么损失。我们可以把a复制给一个指针变量,然后改变该指针变量:

p = a; 
while (*p != 0) p++;

8.1节的程序reverse.c读入10个数,然后逆序输出这些数。程序读取数时会把这些数存入数组。一旦所有的数都读入了,程序就会反向遍历数组并打印出这些数。

原来的程序利用下标来访问数组中的元素。下面是改进后的程序,我们用指针的算术运算取代了数组的取下标操作。

/*
reverse3.c
--Reverses a series of numbers (pointer version)
*/ #include <stdio.h> 
#define N 10 int main(void) 
{ int a[N], *p; printf("Enter %d numbers: ", N); for (p = a; p < a + N; p++) scanf("%d", p); printf("In reverse order:"); for (p = a + N - 1; p >= a; p--) printf(" %d", *p); printf("\n"); return 0; 
} 

在原先的程序中,整型变量i用来记录数组内的当前位置。新版程序用指针变量p替换了i。读入的数仍然存储在数组中,只是换了一种方法来记录数组中的位置。

注意,scanf函数的第二个实际参数是p,不是&p。因为p指向数组的元素,所以它是满足scanf函数要求的参数;而&p则是指向“指向数组元素的指针”的指针。

12.3.1 数组型实际参数(改进版)

数组名在传递给函数时,总是被视为指针。思考下面的函数,这个函数会返回整型数组中最大的元素:


int find_largest(int a[], int n) 
{ int i, max; max = a[0]; for (i = 1; i < n; i++) if (a[i] > max) max = a[i]; return max; 
} 

假设调用find_largest函数如下:

largest = find_largest(b, N);  

这个调用会把指向数组b第一个元素的指针赋值给a数组本身并没有被复制。

把数组型形式参数看作指针会产生许多重要的结果。

  • 在给函数传递普通变量时,变量的值会被复制。任何对相应的形式参数的改变都不会影响到变量。反之,因为没有对数组本身进行复制,所以作为实际参数的数组是可能被改变的。例如,下列函数(9.3节见过)可以通过在数组的每个元素中存储零来修改数组:

    void store_zeros(int a[], int n) 
    { int i; for (i = 0; i < n; i++) a[i] = 0; 
    } 
    

    为了指明数组型形式参数不会被改变,可以在其声明中包含单词const

    int find_largest(const int a[], int n) 
    { ... 
    } 
    

    如果参数中有const,编译器会核实find_largest函数体中确实没有对a中元素的赋值。

  • 给函数传递数组所需的时间与数组的大小无关。因为没有对数组进行复制,所以传递大数组不会产生不利的结果。

  • 如果需要,可以把数组型形式参数声明为指针。例如,可以按如下形式定义find_largest函数:

    int find_largest(int *a, int n) 
    { ... 
    } 
    

    声明a是指针就相当于声明它是数组。编译器把这两类声明看作完全一样的。

请注意!!对于形式参数而言,声明为数组跟声明为指针是一样的;但是对变量而言,声明为数组跟声明为指针是不同的。声明

int a[10];

会导致编译器预留10个整数的空间,但声明

int *a;

只会导致编译器为一个指针变量分配空间。在后一种情况下,a不是数组,试图把它当作数组来使用可能会导致极糟的后果。例如,赋值

*a = 0; /* wrong */

将在a指向的地方存储0。因为我们不知道a指向哪里,所以对程序的影响是无法预料的。

  • 可以给形式参数为数组的函数传递数组的“片段”,所谓片段是指连续的数组元素组成的序列。假设希望用find_largest函数来定位数组b中某一部分的最大元素,比如元素b[5],…,b[14]。调用find_largest函数时,将传递b[5]的地址和数10,表明希望find_largest函数从b[5]开始检查10个数组元素:
    largest = find_largest(&b[5], 10); 
    

12.3.2 用指针作为数组名

既然可以用数组名作为指针,C语言是否允许把指针看作数组名进行取下标操作呢?现在,你可能猜出答案是肯定的,你是对的。下面是一个例子:

#define N 10 
... 
int a[N], i, sum = 0, *p = a; 
...  
for (i = 0; i < N; i++) sum += p[i];

编译器把p[i]看作*(p+i),这是指针算术运算非常正规的用法。目前我们对能够对指针取下标还仅限于好奇,但17.3节会看到它实际上非常有用。

12.4 指针和多维数组

就像指针可以指向一维数组的元素一样,指针还可以指向多维数组的元素。本节将探讨用指针处理多维数组元素的常用方法。简单起见,这里只讨论二维数组,但所有内容都可以应用于更高维的数组。

12.4.1 处理多维数组的元素

8.2节可知,C语言按行主序存储二维数组;换句话说,先是第0行的元素,接着是第1行的,依此类推。

使用指针时可以利用这一布局特点。如果使指针p指向二维数组中的第一个元素(即第0行第0列的元素),就可以通过重复自增p的方法访问数组中的每一个元素。

作为示例,一起来看看把二维数组的所有元素初始化为0的问题。假设数组的声明如下:

int a[NUM_ROWS][NUM_COLS];

显而易见的方法是用嵌套的for循环:

int row, col; 
... 
for (row = 0; row < NUM_ROWS; row++) for (col = 0; col < NUM_COLS; col++) a[row][col] = 0;

但是,如果把a看作一维的整型数组,那么就可以把上述两个循环改成一个循环了:

int *p; 
... 
for (p = &a[0][0]; p <= &a[NUM_ROWS-1][NUM_COLS-1]; p++) *p = 0;

循环开始时p指向a[0][0]。对p连续自增可以使指针p指向a[0][1]a[0][2]a[0][3]等。当p到达a[0][NUM_COLS-1](即第0行的最后一个元素)时,再次对p自增将使它指向a[1][0],也就是第1行的第一个元素。这一过程持续进行,直到p越过a[NUM_ROWS-1][NUM_COLS-1](数组中的最后一个元素)为止。

虽然把二维数组当成一维数组来处理看上去像在搞欺骗,但是对大多数C语言编译器而言这样做是合法的。这样做是否是个好主意则要另当别论。这类方法明显破坏了程序的可读性,但是至少对一些老的编译器来说,这种方法在效率方面进行了补偿。不过,对许多现代的编译器来说,这样所获得的速度优势往往极少,甚至完全没有。

12.4.2 处理多维数组的行

处理二维数组的一行中的元素,该怎么办呢?再次选择使用指针变量p。为了访问到第i行的元素,需要初始化p使其指向数组a中第i行的元素0

p = &a[i][0]; 

对于任意的二维数组a来说,由于表达式a[i]是指向第i行中第一个元素(元素0)的指针,上面的语句可以简写为:

p = a[i];

为了了解原理,回顾一下把数组取下标和指针算术运算关联起来的那个神奇公式:对于任意数组a来说,表达式a[i]等价于*(a + i)。因此&a[i][0]等同于&(*(a[i] + 0)),而后者等价于&*a[i];又因为&*运算符可以抵消,所以也就等同于a[i]。下面的循环对数组a的第i行清零,其中用到了这一简化:

int a[NUM_ROWS][NUM_COLS], *p, i; 
... 
for (p = a[i]; p < a[i] + NUM_COLS; p++) *p = 0; 

因为a[i]是指向数组a的第i行的指针,所以可以把a[i]传递给需要用一维数组作为实际参数的函数。换句话说,使用一维数组的函数也可以使用二维数组中的一行。因此,诸如find_largeststore_zeros这类函数比我们预期的更加通用。思考最初设计用来找到一维数组中最大元素的find_largest函数,现在同样可以用它来确定二维数组a中第i行的最大元素:

largest = find_largest(a[i], NUM_COLS); 

12.4.3 处理多维数组的列

处理二维数组的一列中的元素就没那么容易了,因为数组是按行而不是按列存储的。下面的循环对数组a的第i列清零:

int a[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS], i; 
... 
for (p = &a[0]; p < &a[NUM_ROWS]; p++) (*p)[i] = 0; 

这里把p声明为指向长度为NUM_COLS的整型数组的指针。在(*p)[NUM_COLS]中,*p是需要使用括号的;如果没有括号,编译器将认为p指针数组,而不是指向数组的指针。表达式p++p移到下一行的开始位置。在表达式(*p)[i]中,*p代表a的一整行,因此(*p)[i]选中了该行第i列的那个元素。(*p)[i]中的括号是必要的,因为编译器会将*p[i]解释为*(p[i])

12.4.4 用多维数组名作为指针

就像一维数组的名字可以用作指针一样,无论数组的维数是多少都可以采用任意数组的名字作为指针。但是,需要特别小心。思考下列数组:

int a[NUM_ROWS][NUM_COLS]; 

a不是指向a[0][0]的指针,而是指向a[0]的指针。从C语言的角度来看,这样做是有意义的。C语言认为a不是二维数组而是一维数组,并且这个一维数组的每个元素又是一维数组。用作指针时,a的类型是int (*)[NUM_COLS](指向长度为NUM_COLS的整型数组的指针)。

了解a指向的是a[0]有助于简化处理二维数组元素的循环。例如,为了把数组a的第i列清零,可以用

for (p = &a[0]; p < &a [NUM_ROWS]; p++) (*p)[i] = 0;

取代

for (p = a; p < a + NUM_ROWS; p++) (*p)[i] = 0;

另一种应用是巧妙地让函数把多维数组看作一维数组。例如,思考如何使用find_largest函数找到二维数组a中的最大元素。我们把a(数组的地址)作为find_largest函数的第一个实际参数,NUM_ROWS * NUM_COLS(数组a中的元素总数量)作为第二个实际参数:

largest = find_largest(a, NUM_ROWS * NUM_COLS);    /* WRONG */ 

这条语句不能通过编译,因为a的类型为int (*)[NUM_COLS],但find_largest函数期望的实际参数类型是int *。正确的调用是

largest = find_largest(a[0], NUM_ROWS * NUM_COLS); //a[0]指向第0行的元素0,类型为int *(编译器转换以后),所以这一次调用将正确地执行。 

12.5 C99中的指针和变长数组

指针可以指向变长数组(8.3节)中的元素,变长数组是C99的一个特性。普通的指针变量可以用于指向一维变长数组的元素:

void f(int n) 
{ int a[n], *p; p = a; ...  
}

如果变长数组是多维的,指针的类型取决于除第一维外每一维的长度。下面是二维的情况:

void f(int m, int n) 
{ int a[m][n], (*p)[n]; p = a; ...   
}

因为p的类型依赖于n,而n不是常量,所以说p具有变量修改类型。需要注意的是,编译器并非总能确定p = a这样的赋值语句的合法性。例如,下面的代码可以通过编译,但只有当m = n时才是正确的:

int a[m][n], (*p)[m]; 
p = a; 

如果m≠n,后续对p的使用都将导致未定义的行为。


与变长数组一样,变量修改类型也具有特定的限制,其中最重要的限制是,变量修改类型的声明必须出现在函数体内部或者在函数原型中

变长数组中的指针算术运算和一般数组中的指针算术运算一样。回到12.4节中那个对二维数组a的一列进行清零操作的例子,这次将二维数组a声明为变长数组:

int a[m][n];

指向数组a中某行的指针可以声明为:

int (*p)[n];

把第i列清零的循环几乎跟12.4节中的完全一样:

for (p = a; p < a + m; p++) (*p)[i] = 0; 

问与答

问1:我不理解指针的算术运算。如果指针是地址,那么这是否意味着p + j这样的表达式是把j加到存储在p中的地址上呢?

答:不是的。用于指针算术运算的整数需要根据指针的类型进行缩放。例如,如果p的类型是int *,那么p + j通常给p加上4 × j(假定int类型的值要用4字节存储)。但是,如果p的类型为double *,那么p + j可能给p加上8 × j,因为double类型的值通常是8字节长。

问2:编写处理数组的循环时,数组取下标和指针算术运算哪种更好一些呢?

答:这个问题不容易回答,因为答案与所使用的机器和编译器有关。对于早期PDP-11机器上的C语言,指针算术运算能生成更快的程序。如果在现在的机器上采用现在的编译器,数组取下标方法常常跟指针算术运算差不多,而且有时甚至会更好。底线是学习这两种方法,然后采用对你正在编写的程序更自然的方法。

问3:我在某些地方看到i[a]a[i]是一样的,这是真的吗?

答:是的,这是真的,确实很奇怪。对于编译器而言i[a]等同于*(i + a),也就是*(a + i)(像普通加法一样,指针加法也是可交换的)。而*(a + i)也就是a[i]。但是请不要在程序中使用i[a],除非你正计划参加下一届C语言混乱代码大赛。

问4:为什么在形式参数的声明中*aa[]是一样的?

答:上述这两种形式都说明我们期望实际参数是指针。在这两种情况下,对a可进行的运算是相同的(特别是指针算术运算和数组取下标运算)。而且,在这两种情况下,可以在函数内给a本身赋予新的值。(C语言要求数组变量的名字只能用作“常量指针”,但对于数组型形式参数的名字没有这一限制。)

问5:把数组型形式参数声明为*aa[]哪种风格更好呢?

答:这个问题很棘手。一种观点认为,因为*a是不明确的(函数到底需要多对象的数组还是指向单个对象的指针?),所以a[]更好是显而易见的。但是,许多程序员认为把形式参数声明为*a更准确,因为它会提醒我们传递的仅仅是指针而不是数组的副本。有些人则根据具体情况在两种风格之间切换,切换的依据是函数是使用指针算术运算还是使用取下标运算来访问数组的元素的。(本书也采用这种方法。)在实践中,*aa[]更常用,所以最好习惯于前者。不知道是真是假,听说现在Dennis Ritchiea[]标记称为“活化石”,因为它“在使学习者困惑方面起的作用与在提醒程序阅读者方面所起的作用是相同的”。

问6:我们已经看到C语言中数组和指针之间的紧密联系。称它们是可互换的是否准确?

答:不准确数组型形式参数和指针形式参数是可以互换的,但是数组型变量不同于指针变量。从技术上说,数组的名字不是指针,C语言编译器会在需要时把数组的名字转换为指针。为了更清楚地看出两者的区别,思考对数组a使用sizeof运算符时会发生什么。sizeof(a)的值是数组中字节的总数,即每个元素的大小乘以元素的数量。但是,如果p是指针变量,那么sizeof(p)的值则是用来存储指针值所需的字节数量。

问7:书上说把二维数组视为一维数组对“大多数”编译器而言是合法的。难道不是对所有编译器都合法吗?

答:不能说对所有编译器都合法。一些现代的“越界检查”编译器不仅记录指针的类型,还会在指针指向数组时记录数组的长度。例如,假设给p赋一个指向a[0][0]的指针。从技术上讲,p指向的是一维数组a[0]的第一个元素。如果在遍历a的所有元素的过程中反复对p进行自增操作,当p越过a[0]的最后一个元素时我们就越界了。执行越界检查的编译器会插入代码,验证p只能用于访问a[0]指向的数组中的元素。一旦越过这个数组的边界,再对p进行自增就会导致编译器报错。

问8:如果a是二维数组,为什么可以给find_largest函数传递a[0]而不是数组a本身呢?aa[0]不是都指向同一位置(数组开始的位置)吗?

答:它们确实指向同一位置,两者都指向元素a[0][0]问题是a的类型不对。用作实际参数时,a是一个指向数组的指针,但find_largest函数需要指向整数的指针作为参数。a[0]的类型为int *,因此它可以作为find_largest函数的实际参数。关于类型的这种考虑实际上是很好的,如果C语言没这么挑剔,我们可能会犯各种各样编译器注意不到的指针错误。


写在最后

本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/116118.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

js关于深度克隆问题

js的克隆是一个老生常谈的内容了,今天没啥好写的,就写这个了 要搞清楚js的克隆,就需要先搞清楚js中的数据类型,js中数据类型分为两大类 类型说明原始类型-string字符串类型&#xff0c;用于表示文本数据。number数字类型&#xff0c;包括整数和浮点数&#xff0c;用于表示数值…

负载均衡的算法(静态算法与动态算法)

1.静态算法 静态算法是不考虑服务器动态负载的算法&#xff0c;包括&#xff1a; &#xff08;1&#xff09;轮转算法&#xff1a;轮流将服务请求&#xff08;任务&#xff09;调度给不同的节点&#xff08;即&#xff1a;服务器&#xff09;。 &#xff08;2&#xff09;加…

app拉新渠道整合 一手地推、网推拉新平台整理

1.聚量推客 聚量推客自己本身是服务商&#xff0c;自己直营的平台&#xff0c;相对来说数据更好&#xff0c;我们也拿到了平台首码&#xff1a;000000 填这个就行&#xff0c;属于官方渠道 2.蓝猫推客 蓝猫推客我认为是比较又潜力的平台&#xff0c;经过几天测试数据和结算都…

网络规划设计

文章目录 一、网络架构设计1.1 单核心双核心1.1.1 单核心1.1.2 双核心端口聚合 链路聚合网关冗余VRRP堆叠技术生成树STPPOE以太网供电服务器冗余&负载均衡服务器双机热备 1.2 环网架构设计1.3 层次化架构设计1.3.2 三层网络架构设计1.3.3 园区大二层网络架构1.3.4 数据中心…

在 Node.js 中实现基于角色的访问控制

在 Node.js 中实现基于角色的访问控制 基于角色的访问控制 (Role-Based Access Control&#xff0c;缩写RBAC) 是应用程序安全性的一个重要方面。它提供了一种结构化方法&#xff0c;可以根据组织或应用程序中用户的角色来管理和限制对资源的访问。在本文中&#xff0c;我们将…

最详细STM32,cubeMX串口发送,接收数据

这篇文章将详细介绍 串口 发送数据&#xff0c;接受数据。 文章目录 前言一、串口的基础知识二、cubeMX 配置三、自动生成代码解析四、串口发送数据函数五、使用串口收发数据点亮 led重定向函数&#xff1a; 总结 前言 实验开发板&#xff1a;STM32F103C8T6。所需软件&#xf…

【TES605】基于Virtex-7 FPGA的高性能实时信号处理平台

板卡概述 TES605是一款基于Virtex-7 FPGA的高性能实时信号处理平台&#xff0c;该平台采用1片TI的KeyStone系列多核DSP TMS320C6678作为主处理单元&#xff0c;采用1片Xilinx的Virtex-7系列FPGA XC7VX690T作为协处理单元&#xff0c;具有2个FMC子卡接口&#xff0c;各个处理节…

景联文科技:针对敏感数据的安全转录服务,护航信息安全

针对数据的安全转录服务&#xff0c;主要是为了确保数据在转录过程中的安全性和隐私保护。这些服务通常会采用一系列严格的安全措施&#xff0c;如数据加密、访问控制、数据脱敏等&#xff0c;以确保敏感数据不会被泄露或滥用。 景联文科技提供特定的数据转录服务&#xff0c;以…

CentOS7.9+Kubernetes1.28.3+Docker24.0.6高可用集群二进制部署

CentOS7.9Kubernetes1.28.3Docker24.0.6高可用集群二进制部署 查看版本关系 ## 从kubernetes-server-linux-amd64.tar.gz解压后有kubeadm ]# ./kubeadm config images list W1022 20:06:05.647976 29233 version.go:104] could not fetch a Kubernetes version from the in…

WPF Material Design UI框架

前言 Material Design in xaml 是开源免费的ui框架&#xff0c;工控软件主打的就是简单界面。 以下简称MD 相关资源 MaterialDesignInXamlToolkit Github 地址 MD 快速启动 MD 案例压缩包 MD 框架使用 启动环境配置 安装Nuget包 App.xaml 配置 <Application x:Class&qu…

leetcode:面试题 17.04. 消失的数字(找单身狗/排序/公式)

一、题目&#xff1a; 函数原型&#xff1a;int missingNumber(int* nums, int numsSize) 二、思路&#xff1a; 思路1 利用“找单身狗”的思路&#xff08;n^n0&#xff1b;0^nn&#xff09;&#xff0c;数组中有0-n的数字&#xff0c;但缺失了一个数字x。将这些数字按位异或0…

zookeeper源码(02)源码编译启动及idea导入

本文介绍一下zookeeper-3.9.0源码下载、编译及本地启动。 下载源码 git clone https://gitee.com/apache/zookeeper.gitcd zookeeper git checkout release-3.9.0 git checkout -b release-3.9.0源码编译 README_packaging.md文件 该文件介绍了编译zookeeper需要的环境和命…

PKU 概率论+数理统计+建模 期中考复习总结

目录 计算条件概率计算概率&#xff08;放回与不放回&#xff09;生成随机数算法Linear Congruential Method判断是否是full period Uniformity (test of frequency)1.Chi-Square testmethodreminderexample 2.Kolmogorov-Sminov testmethodexample Independence (test of auto…

决策树-入门

1、认识决策树 决策树思想的来源非常朴素&#xff0c;程序设计中的条件分支结构就是if-then结构&#xff0c;最早的决策树就是利用这类结构分割数据的一种分类学习方法 怎么理解这句话&#xff1f;通过一个对话例子 想一想这个女生为什么把年龄放在最上面判断&#xff01;&a…

WebSocket—STOMP详解(官方原版)

WebSocket协议定义了两种类型的消息&#xff08;文本和二进制&#xff09;&#xff0c;但其内容未作定义。该协议定义了一种机制&#xff0c;供客户端和服务器协商在WebSocket之上使用的子协议&#xff08;即更高级别的消息传递协议&#xff09;&#xff0c;以定义各自可以发送…

【Java笔试强训】Day4(WY33 计算糖果、DD5 进制转换)

WY33 计算糖果 链接&#xff1a;WY33 计算糖果 题目&#xff1a; A,B,C三个人是好朋友,每个人手里都有一些糖果,我们不知道他们每个人手上具体有多少个糖果,但是我们知道以下的信息&#xff1a; A - B, B - C, A B, B C. 这四个数值.每个字母代表每个人所拥有的糖果数. 现…

Win10/Win11系统bitlocker正在等待激活如何解决?

有同学升级Win10系统后&#xff0c;发现C盘与D盘分区盘符中出现了黄色的锁定感叹号&#xff0c;还显示“bitlocker正在等待激活”&#xff0c;这可能是用户开启了bitlocker加密所导致的。下面就来看看解决的办法吧。 一、bitlocker正在等待激活的解决方法 打开控制面板-系统和安…

.obj模型文件(带材质和纹理)合并的基本思路

1、将v开头的顶点信息依次拷贝到合并新.obj中 2、将vt纹理坐标依次拷贝到合并新.obj中 3、f&#xff08;面&#xff09;的合并 步骤&#xff1a; &#xff08;1&#xff09;第一个obj文件的f&#xff08;面&#xff09;原封不动拷进新.obj中 &#xff08;2&#xff09;第二个…

【git 学习】--- ubuntu18.04 搭建本地git服务器

在Ubuntu18.04 上简单创建自己的git服务器~ 环境配置 Ubuntu: 18.04git服务器搭建步骤&#xff1a; ##1.安装git sudo apt-get install git##2.添加用户 sudo adduser test_git //test_git -- git用户名##3. 在Git用户的home目录下创建文件夹&#xff0c;作为裸仓库 sudo…

【虚幻引擎UE】UE4/UE5 基于2D屏幕坐标获取场景3D坐标 射线检测(蓝图/C++)

UE4/UE5 基于2D屏幕坐标获取场景3D坐标 一、射线检测1&#xff09;定义1&#xff09;射线与3D场景中的物体交互的流程2&#xff09;射线检测蓝图函数3&#xff09;蓝图实现根据鼠标点击位置获取场景中的坐标值4&#xff09;根据相机中心点获取场景中的坐标值5&#xff09;射线检…