• C Primer Plus学习笔记(九)- 数组和指针


    数组

    数组由数据类型相同的同一系列元素组成

    需要使用数组时,通过声明数组告诉编译器数组中内含多少元素和这些元素的类型

    普通变量可以使用的类型,数组元素都可以用

    float candy[365];  // 内含 365 个 float 类型元素的数组
    char code[12];  // 内含 12 个 char 类型元素的数组
    int states[50];  // 内含 50 个 int 类型元素的数组
    

    方括号([])表明 candy、code 和 states 都是数组,方括号中的数字表明数组中的元素个数

    要访问数组中的元素,通过使用数组下标数(也称为索引)表示数组中的各元素

    数组元素的编号从 0 开始,所以 candy[0] 表示 candy 数组的第 1 个元素,candy[364] 表示第 365 个元素,也就是最后一个元素

    初始化数组

    只储存单个值的变量有时也称为标量变量(scalar variable)

    int nums[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    

    用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔,逗号和值之间可以使用空格

    根据上面的初始化,把 1 赋给数组的首元素(nums[0]),以此类推

    不支持 ANSI 的编辑器会把这种形式的初始化识别为语法错误,在数组声明前加上关键字 static 可以解决此问题

    使用 const 声明数组

    用 const 声明和初始化数组的话,这个数组就是只读数组,程序在运行过程中不能修改数组中的内容

    const int nums[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    

    和普通变量一样,应该使用声明来初始化 const 数据

    如果初始化数组失败的话

    #include <stdio.h>
    #define SIZE 4
    
    int main(void)
    {
    	int no_data[SIZE];  // 未初始化数组
    	int i;
    
    	printf("%2s%14s
    ", "i", "no_data[i]");
    	for (i = 0; i < SIZE; i++)
    		printf("%2d%14d
    ", i, no_data[i]);
    
    	return 0;
    }
    

    运行结果

    使用数组前必须先初始化它。与普通变量类似,在使用数组元素之前,必须先给它们赋初值

    编译器使用的值是内存相应位置上的现有值,系统不同,输出的结果可能不同

    初始化列表中的项数应与数组的大小一致

    #include <stdio.h>
    #define SIZE 4
    
    int main(void)
    {
    	int some_data[SIZE] = {2, 4};
    	int i;
    
    	printf("%2s%14s
    ", "i", "some_data[i]");
    	for (i = 0; i < SIZE; i++)
    		printf("%2d%14d
    ", i, some_data[i]);
    
    	return 0;
    }
    

    运行结果

    当初始化列表中的值少于数组元素个数时,编译器会把剩余的元素都初始化为 0

    如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值;如果部分初始化数组,剩余的元素就会被初始化为 0

    可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数

    #include <stdio.h>
    
    int main(void)
    {
    	const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31};
    	int i;
    
    	for (int i = 0; i < sizeof days / sizeof days[0]; i++)
    		printf("Month %2d has %d days.
    ", i + 1, days[i]);
    
    	return 0;
    }
    

    运行结果

    sizeof days 是整个数组的大小(以字节为单位),sizeof days[0] 是数组中一个元素的大小(以字节为单位)

    整个数组的大小除以单个元素的大小就是数组元素的个数

    指定初始化器(C99)

    C99 增加了一个新特性:指定初始化器(designated initializer),利用该特性可以初始化指定的数组元素

    C99 规定,可以在初始化列表中使用带方括号的小标指明待初始化的元素

    int arr[6] = {[5] = 212};  // 把 arr[5] 初始化为 212
    

    对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为 0

    #include <stdio.h>
    #define MONTHS 12
    
    int main()
    {
    	int days[MONTHS] = {31, 28, [4] = 31, 30, 31, [1] = 29};
    	int i;
    
    	for (i = 0; i < MONTHS; i++)
    		printf("%2d %d
    ", i + 1, days[i]);
    
    	return 0;
    }
    

    运行结果

    如果指定初始化器后面有更多的值,那么后面的这些值将被用于初始化指定元素后面的元素

    [4] = 31, 30, 31,在 days[4] 被初始化为 31 后,days[5] 和 days[6] 将分别被初始化为 30 和 31

    如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化

    如果没有指定元素的大小

    int num[] = {1, [6] = 23};
    int days[] = {1, [6] = 4, 9, 10};
    

    编译器会把数组的大小设置为足够装得下初始化的值

    num 数组有 7 个元素,编号为 0~6;days 数组有 9 个元素

    给数组元素赋值

    声明数组后,可以借助数组下标(或索引)给数组元素赋值

    C 不允许把数组作为一个单元赋给另一个数组,除初始化外也不允许使用花括号列表的形式赋值

    #define SIZE 5
    
    int main(void)
    {
    	int oxen[SIZE] = {5, 3, 2, 8};
    	int yaks[SIZE];
    
    	yaks = oxen;                 // 不允许
    	yaks[SIZE] = oxen[SIZE];     // 数组下标越界
    	yaks[SIZE] = {5, 3, 2, 8};   // 不起作用
    }
    

    oxen 数组的最后一个元素是 oxen[SIZE-1],所以 oxen[SIZE] 和 yaks [SIZE] 都超出了两个数组的末尾

    数组边界

    在使用数组时,要防止数组下标超出边界,必须确保下标是有效的值

    编译器不会检查数组下标是否使用得当

    在 C 标准中,使用越界下标的结果是未定义的

    #include <stdio.h>
    #define SIZE 4
    
    int main(void)
    {
    	int value1 = 44;
    	int arr[SIZE];
    	int value2 = 88;
    	int i;
    
    	printf("value1 = %d, value2 = %d
    ", value1, value2);
    	for (i = -1;i <= SIZE; i++)
    		arr[i] = 2 * i + 1;
    
    	for (i = -1; i < 7; i++)
    		printf("%2d %d
    ", i, arr[i]);
    	printf("value1 = %d, value2 = %d
    ", value1, value2);
    	printf("address of arr[-1]: %p
    ", &arr[-1]);
    	printf("address of arr[4]: %p
    ", &arr[4]);
    	printf("address of value1: %p
    ", &value1);
    	printf("address of value2: %p
    ", &value2);
    
    	return 0;
    }
    

    运行结果

    使用越界的数组下标会导致程序改变其他变量的值,不同的编译器运行该程序的结果可能不同,有些会导致程序异常中止

    数组元素的编号从 0 开始,最好是在声明数组时使用符号常量来表示数组的大小

    指定数组的大小

    在 C99 标准之前,声明数组时只能在方括号中使用整型常量表达式

    整型常量表达式,是由整型常量构成的表达式

    sizeof 表达式被视为整型常量,但是 const 值不是(与 C++ 不同)

    表达式的值必须大于 0

    int n = 5;
    int m = 8;
    float a1[5];                 // 可以
    float a2[5*2 + 1];           // 可以
    float a3[sizeof(int) + 1];   // 可以
    float a4[-4];                // 不可以,数组大小必须大于 0
    float a5[0];                 // 不可以,数组大小必须大于 0
    float a6[2.5];               // 不可以,数组大小必须是整数
    float a7[(int)2.5];          // 可以,已被强制转换为整型常量
    float a8[n];                 // C99 之前不允许
    float a9[m];                 // C99 之前不允许
    

    C99 标准允许后两种的声明,这创建了一种新型数组,称为变长数组(variable-length array)或简称 VLA(C11 把 VLA 设定为可选,而不是语言必备的特性)

    声明 VLA 时不能进行初始化

    多维数组

    初始化二维数组

    初始化二维数组是建立在初始化一维数组的基础上

    const int dates[YEARS][MONTHS] = 
    {
    	{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    	{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    	{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
    	{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
    }
    

    初始化二维数组 dates

    这个初始化使用了 5 个数值列表,每个数值列表都用花括号括起来

    如果某列表中的数值个数超出了数组每行的元素个数,则会出错,但是并不会影响其他行的初始化

    初始化时可以省略内部的花括号,只保留最外面的一堆花括号,要保证初始化的数值个数正确

    如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值,后面没有值初始化的元素被统一初始化为 0

    其他多维数组

    声明一个三维数组:

    int nums[10][20][30];
    

    nums 内含 10 个元素,每个元素是内含 20 个元素的数组,这 20 个数组元素中的每个元素是内含 30 个元素的数组

    处理三维数组要使用 3 重嵌套循环,处理四维数组要使用 4 重嵌套循环。对于其他多维数组,以此类推

    查找地址:& 运算符

    指针(pointer)是 C 语言最重要的也是最复杂的概念之一,用于储存变量的地址

    如果主调函数不使用 return 返回的值,则必须通过地址才能修改主调函数中的值

    一元 & 运算符给出变量的存储地址

    如果 pooh 是变量名,那么 &pooh 是变量的地址

    可以把地址看作是变量在内存中的位置

    int pooh = 24;
    printf("%d %p
    ", pooh, &pooh);
    

    输出的内容为

    24 是变量 pooh 的值,0061FF2C 是变量 pooh 的存储地址

    PC 地址通常用十六进制形式表示,每个十六进制数对应 4 位

    指针简介

    指针是一个值为内存地址的变量(或数据对象)

    char 类型变量的值是字符,int 类型变量的值是整数,指针变量的值是地址

    假设一个指针变量名是 ptr

    ptr = &pooh;  // 把 pooh 的地址赋给 ptr
    

    对于这条语句,我们说 ptr “指向” pooh

    ptr 和 &pooh 的区别是 ptr 是变量,而 &pooh 是常量

    要创建指针变量,先要声明指针变量的类型

    间接运算符:*

    假设已知 ptr 指向 bah

    ptr = &bah;
    

    然后使用间接运算符 *(indirection operator)找出储存在 bah 中的值,该运算符有时也称为解引用运算符(dereferencing operator)

    int pooh = 24;
    int * ptr;    // 声明一个指针变量 ptr
    int val;
    ptr = &pooh;  // 把 pooh 的地址赋给 ptr
    val = * ptr;  // 把 ptr 指向的值赋给变量 val
    printf("%d
    ", val);
    

    输出结果

    语句 ptr = &pooh; 和 val = * ptr; 放在一起相当于语句:val = pooh

    最终结果就是把 24 赋给变量 val

    声明指针

    声明指针必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小

    程序必须知道储存在指定地址上的数据类型。long 和 float 可能占用相同的存储空间,但是它们储存数字却大相径庭

    int * pi;   // pi 是指向 int 类型变量的指针
    char * pc;  // pc 是指向 char 类型变量的指针
    float * pf, * pg;  // pf、pg 都是指向 float 类型变量的指针
    

    类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针

    int * pi;
    

    声明的意思是 pi 是一个指针,*pi 是 int 类型

    * 和指针名之间的空格可有可无。通常,在声明时使用空格,在解引用变量时省略空格

    指针实际上是一个新类型,不是整数类型

    指针和数组

    数组名是数组首元素的地址

    如果 days 是一个数组,下面的语句成立:

    days == &days[0];  // 数组名是该数组首元素的地址
    

    days 和 &days[0] 都表示数组首元素的内存地址(& 是地址运算符),两者都是常量,在程序的运行过程中,不会改变

    可以把它们赋值给指针变量,然后可以修改指针变量的值

    #include <stdio.h>
    #define SIZE 4
    
    int main(void)
    {
    	short dates[SIZE];
    	short * pti;
    	short index;
    	double bills[SIZE];
    	double * ptf;
    	pti = dates;
    	ptf = bills;
    
    	printf("%23s %10s
    ", "short", "double");
    
    	for (index = 0; index < SIZE; index++)
    		printf("pointers + %d: %10p %10p
    ", index, pti + index, ptf + index);
    
    	return 0;
    }
    

    运行结果

    地址按字节编址,short 类型占用 2 字节,double 类型占用 8 字节

    short:0061FF1C + 2 为 0061FF1E

    double:0061FEF8 + 8 为 0061FF00

    在 C 中,指针加 1 指的是增加一个存储单元。对数组而言,这意味着加 1 后的地址是下一个元素的地址,而不是下一个字节的地址

    指针指向的是标量变量,也要知道变量的类型,否则 *pti 就无法正确地取回地址上的值

    指针的值是它所指向对象的地址。地址的表达方式依赖于计算机内部的硬件。许多计算机都是按字节编址,意思是内存中的每个字节都按顺序编号。一个较大对象的地址通常是该对象第一个字节的地址

    在指针前面使用 * 运算符可以得到该指针所指向对象的值

    指针加 1,指针的值递增它所指向类型的大小(以字节为单位)

    假定 days 是一个一维数组

    days + 2 == &days[2];    // 相同的地址
    *(days + 2) == dates[2]  // 相同的值
    

    C 语言标准在描述数组表示法时借助了指针

    定义 ar[n] 的意思是 *(ar + n),*(ar + n) 的意思是“到内存的 ar 位置,然后移动 n 个单元”

    间接运算符(*)的优先级高于 +,所以 *days+2 相当于 (*days) + 2,*(days + 2) 是 days 第 3 个元素的值,*days + 2 是 days 第 1 个元素的值加 2

    #include <stdio.h>
    #define MONTHS 12
    
    int main()
    {
    	int days[MONTHS] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    	int index;
    
    	for (index = 0; index < MONTHS; index++)
    		printf("Month %2d has %d days.
    ", index + 1, *(days + index));  // *(days + index) 与 days[index]相同
    
    	return 0;
    }
    

    运行结果

    days 是数组首元素的地址,days + index 是元素 days[index] 的地址,而 *(days + index) 则是该元素的值,相当于 days[index]

    函数、数组和指针

    只有在函数原型或函数定义头中,才可以使用 int ar[] 代替 int * ar

    int sum(int ar[], int n);
    

    int *ar 形式和 int ar[] 形式都表示 ar 是一个指向 int 的指针。但是,int ar[] 只能用于声明形式参数

    int ar[] ,指针 ar 指向的不仅仅一个 int 类型值,还是一个 int 类型数组的元素

    因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。只有在这种情况下,C 才会把 int ar[] 和 int * ar  解释成一样的

    由于函数原型可以省略参数名,所以下面 4 种原型都是等价的:

    int sum(int *ar, int n);
    int sum(int *, int);
    int sum(int ar[], int n);
    int sum(int [], int);
    

    但是,在函数定义中不能省略参数名

    int sum(int *ar, int n)
    {
    }
    

    相当于

    int sum(int ar[], int n)
    {
    }
    

    如果系统中用 8 字节储存地址,指针变量的大小为 8 字节

    使用指针形参

    函数要处理数组必须知道何时开始,何时结束

    给函数传递两个指针,第 1 个指针指明数组的开始处,第 2 个指针指明数组的结束处

    指针操作

    指针变量的 8 种基本操作

    #include <stdio.h>
    
    int main(void)
    {
    	int urn[5] = {100, 200, 300, 400, 500};
    	int *ptr1, *ptr2, *ptr3;
    
    	ptr1 = urn;          // 把一个地址赋给一个指针
    	ptr2 = &urn[2];      // 把一个地址赋给一个指针
    
    	printf("pointer value, dereferenced pointer, pointer address:
    ");
    	printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p
    ", ptr1, *ptr1, &ptr1);
    
    	// 指针加法
    	ptr3 = ptr1 + 4;
    	printf("
    adding an int to a pointer:
    ");
    	printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d
    ", ptr1 + 4, *(ptr1 + 4));
    	ptr1++;             // 递增指针
    	printf("
    values after ptr1++:
    ");
    	printf("ptr1 = %p, *ptr1 = %d, &ptr1 = %p
    ", ptr1, *ptr1, &ptr1);
    	ptr2--;             // 递减指针
    	printf("
    values after ptr2--:
    ");
    	printf("ptr2 = %p, *ptr2 = %d, &ptr2 = %p
    ", ptr2, *ptr2, &ptr2);
    	--ptr1;             // 恢复为初始值
    	++ptr2;             // 恢复为初始值
    	printf("
    Pointers reset to original values:
    ");
    	printf("ptr1 = %p, ptr2 = %p
    ", ptr1, ptr2);
    	// 一个指针减去另一个指针
    	printf("
    subtracting one pointer from another:
    ");
    	printf("ptr2 = %p, ptr1 = %p, ptr2 - ptr1 = %ld
    ", ptr2, ptr1, ptr2 - ptr1);
    	// 一个指针减去一个整数
    	printf("
    subtracting an int from a pointer:
    ");
    	printf("ptr3 = %p, ptr3 - 2 = %p
    ", ptr3, ptr3 - 2);
    
    	return 0;
    }
    

    运行结果

    指针变量的基本操作:

    赋值:可以把地址赋给指针。例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。地址和指针的类型要兼容,不能把 double 类型的地址赋给指向 int 的指针

    解引用:* 运算符给出指针指向地址上的储存的值

    取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址

    指针与整数相加:可以使用 + 运算符把指针与整数相加,或整数与指针相加。无论哪种情况,整数都会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。如果相加的结果超出了初始指针指向的数组范围,计算结果是未定义的。除非正好超过数组末尾第一个位置,C 保证该指针有效

    递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。变量不会因为值发生变化就移动位置

    指针减去一个整数:可以使用 - 运算符从一个指针中减去一个整数。指针必须是第 1 个运算对象,整数是第 2 个运算对象。该整数将乘以指针指向类型的大小(以字节为单位),然后用初始地址减去乘积。如果相减的结果超出了初始指针所指向数组的范围,计算结果则是未定义的。除非正好超过数组末尾第一个位置,C 保证该指针有效

    递减指针:除了递增指针还可以递减指针。前缀和后缀的递增和递减运算符都可以使用

    指针求差:可以计算两个指针的差值。通常,求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。差值的单位与数组类型的单位相同。ptr2 - ptr1 得 2,意思是这两个指针所指向的两个元素相隔两个 int,而不是 2 字节。只有两个指针都指向相同的数组(或者其中一个指针指向数组后面的第 1 个地址),C 都能保证相减运算有效。如果指向两个不同数组的指针进行求差运算可能会得出一个值,或者导致运行时错误

    比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象

    注意:这里的减法有两种。可以用一个指针减去另一个指针得到一个整数,或者用一个指针减去一个整数得到另一个指针

    在递增或递减指针时还要注意一些问题:

    编译器不会检查指针是否仍指向数组元素

    C 只能保证指向数组任意元素的指针和指向数组后面第 1 个位置的指针有效

    如果递增或递减一个指针后超出了这个范围,则是未定义的

    可以解引用指向数组任意元素的指针。即使指针指向数组后面一个位置是有效的,也能解引用这样的越界指针

    千万不要解引用未初始化的指针

    int *pt;  // 未初始化的指针
    *pt = 5;  // 严重的错误
    

    第 2 行的意思是把 5 储存在 pt 指向的位置。但是 pt 未被初始化,其值是一个随机值,所以不知道 5 将储存在何处。

    这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃

    创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。

    因此,在使用指针之前,必须先用已分配的地址初始化它。

    可以用一个现有变量的地址初始化该指针(使用带指针形参的函数时,就属于这种情况),也可以用 malloc() 函数先分配内存

    保护数组中的数据

    只有程序需要在函数中改变传递的数值时,才会传递指针

    对于数组别无选择,必须传递指针

    如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝到新的数组中

    如果把数组的地址传递给函数,让函数直接处理原数组则效率要高

    处理数组的函数通常都需要使用原始数据

    对形式参数使用 const

    如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字 const

    如果编写的函数需要修改数组,在声明数组形参时则不使用 const;如果编写的函数不用修改数组,在声明数组形参时最好使用 const

    指针和多维数组

    int days[4][2];  // 内含 int 数组的数组
    

    数组名 days 是该数组首元素的地址,days 的首元素是一个内含两个 int 值的数组,所以 days 是这个内容两个 int 值的数组的地址

    days[0] 本身是一个内含两个整数的数组,所以 days[0] 的值和它首元素(一个整数)的地址(即 &days[0][0] 的值)相同

    days[0] 是一个占用一个 int 大小对象的地址,而 days 是一个占用两个 int 大小对象的地址

    由于这个整数和内含两个整数的数组都开始与同一个地址,所以 days 和 days[0] 的值相同

    给指针或地址加 1,其值会增加对应类型大小的数值

    days + 1 和 days[0] + 1 的值不同,因为 days 指向的对象占用了两个 int 大小,而 days[0] 指向的对象只占用了一个 int 大小

    解引用一个指针(在指针前使用 * 运算符)或在数组名后使用带下标的 [] 运算符,得到引用对象代表的值

    days[0] 是该数组首元素(days[0][0])的地址,所以 *(days[0]) 表示储存在 days[0][0] 上的值(即一个 int 类型的值)

    *days 代表该数组首元素(days[0]) 的值,但是 days[0] 本身是一个 int 类型值的地址。该值的地址是 &days[0][0],所以 *days 就是 &days[0][0]

    **days 与 *& days[0][0] 等价,相当于 days[0][0],即一个 int 类型的值

    days 是地址的地址,必须解引用两次才能获得原始值

    如果 days[0] 指向一个 int 类型(4 字节)的数据对象,days[0] 加 1,其值加 4(十六进制中,38 + 4 得 3c)。数组名 days 是一个内含 2 个 int 类型值的数组的地址,所以 days 指向一个 8 字节的数据对象。因此,days 加 1,它所指向的地址加 8 字节(十六进制中,38 + 3 得 40)

    指向多维数组的指针

    pz 必须指向一个内含两个 int 类型值的数组,而不是指向一个 int 类型值,声明如下

    int (*pz)[2];  // pz 指向一个内含两个 int 类型值的数组
    

    pz 被声明为指向一个数组的指针,该数组内含两个 int 类型值

    因为 [] 的优先级高于 *,所以需要有括号

    int *pax[2];  // pax 是一个内含两个指针元素的数组,每个元素都指向 int 的指针
    

    由于 [] 优先级高,先与 pax 结合,所以 pax 成为一个内含两个元素的数组,然后 * 表示 pax 数组内含两个指针

    有括号的声明的是一个指向数组(内含两个 int 类型的值)的指针,没有括号的声明了两个指向 int 的指针

    虽然 pz 是一个指针,不是数组名,但是也可以使用 pz[2][1] 这样的写法

    可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名:

    days[m][n] == *(*(days + m) + n)
    pz[m][n] == *(*(pz + m) + n)
    

    指针的兼容性

    指针之间的赋值比数值类型之间的赋值要严格。例如,不用类型转换就可以把 int 类型的值赋给 double 类型的变量,但是两个类型的指针不能这么做

    int x = 20;
    const int y = 23;
    int * p1 = &x;
    const int * p2 = &y;
    const int ** pp2;
    p1 = p2;  // 不安全 -- 把 const 指针赋给非 const 指针
    p2 = p1;  // 有效 -- 把非 const 指针赋给 const 指针
    pp2 = &p1;  // 不安全 -- 嵌套指针类型赋值
    

    把 const 指针赋给非 const 指针不安全,因为这样可以使用新的指针改变 const 指针指向的数据。编译器在编译代码时,可能会给出警告,执行这样的代码是未定义的。但是把非 const 指针赋给 const 指针没问题

    进行两级解引用时,这样的赋值也不安全

    const int **pp2;
    int *p1;
    const int n = 13;
    pp2 = &p1;  // 允许,但是这导致 const 限定符失效(根据第 1 行代码,不能通过 *pp2 修改它所指向的内容)
    *pp2 = &n;  // 有效,两者都声明为 const,但是这将导致 p1 指向 n(*pp2 已被修改)
    *p1 = 10;  // 有效,但是这将改变 n 的值(但是根据第 3 行代码,不能修改 n 的值)
    

    在 Terminal 中使用 gcc 编译包含以上代码的小程序,导致 n 最终的值是 13,但是在相同的系统下使用 clang 来编译,n 最终的值是 10,两个编译器都给出指针类型不兼容的警告

    函数和多维数组

    可以这样声明函数的形参:

    void func(int (*pt)[4]);
    

    当且仅当 pt 是一个函数的形式参数时,可以这样声明:

    void func(int pt[][4]);
    

    第 1 个方括号是空的,空的方括号表明 pt 是一个指针

    下面声明不正确:

    int sum(int ar[][]);  // 错误的声明
    

    编译器会把数组表示法转换成指针表示法。例如,编译器会把 ar[1] 转换成 ar+1。编译器对 ar+1 求值,要知道 ar 所指向的对象大小

    int sum(int ar[][4]);  // 有效声明
    

     表示 ar 指向一个内含 4 个 int 类型值的数组,所以 ar+1 的意思是“该地址加上 16 字节”

    如果第 2 对方括号是空的,编译器就不知道该怎样处理

    也可以在第 1 对方括号中写上大小,但是编译器会忽略该值

    int sum(int ar[3][4]);  // 有效声明,但是 3 将被忽略
    

    与使用 typedef 相比,这种形式方便得多

    typedef int arr4[4];  // arr4 是一个内含 4 个 int 的数组
    typedef arr4 arr3x4[3];  // arr3x4 是一个内含 3 个 arr4 的数组
    int sum(arr3x4 ar);  // 与下面的声明相同
    int sum(int ar[3][4]);  // 与下面的声明相同
    int sum(int ar[][4]);  // 标准形式
    

    一般而言,声明一个指向 N 维数组的指针时,只能省略最左边方括号中的值

    int sum(int ar[][12][20][30]);
    

    第 1 对方括号只用于表明这是一个指针,而其他方括号则用于描述指针所指向数据对象的类型

    上面的声明相当于:

    int sum(int (*ar)[12][20][30]);  // ar 是一个指针
    

    ar 指向一个 12x20x30 的 int 数组

    变长数组(VLA)

    变长数组有一些限制,变长数组必须是自动存储类别,这意味着无论在函数中声明还是作为函数形参声明,都不能使用 static 或 extern 存储类别说明符。而且,不能在声明中初始化它们

    C99/C11 标准允许在声明变长数组时使用 const 变量,所以该数组的定义必须是声明在块中的自动存储类别数组

    变长数组还允许动态内存分配,这说明可以在程序运行时指定数组的大小

    普通 C 数组都是静态内存分配,即在编译时确定数组的大小。由于数组大小是常量,所以编译器在编译时就知道了

    变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:在创建数组时,可以使用变量指定数组的维度

    声明一个带二维变长数组参数的函数

    int sum(int rows, int cols, int ar[rows][cols]);  // ar 是一个变长数组(VLA)
    

    在形参列表中必须在声明 ar 之前先声明 rows 和 cols 这两个形参

    int sum(int ar[rows][cols], int rows, int cols);  // 无效顺序
    

    C99/C11 标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度

    int sum(int, int, int ar[*][*]);  // ar 是一个变长数组(VLA),省略了维度形参名
    

    在函数定义的形参列表中声明的变长数组并未实际创建数组

    变长数组名实际上是一个指针,这说明带变长数组形参的函数实际上是在原始数组中处理数组,因此可以修改传入的数组

    复合字面量

    在 C99 标准以前,对于带数组形参的函数,情况不同,可以传递数组,但是没有等价的数组常量

    C99 新增了复合字面量(compound literal),字面量是除符号常量外的常量

    例如,5 是 int 类型字面量,81.3 是 double 类型的字面量,'Y' 是 char 类型的字面量,"day" 是字符串字面量

    对于数组,复合字面量类似数组初始化列表,前面是用括号括起来的类型名

    一个普通的数组声明:

    int days[2] = {10, 20};
    

    下面的复合字面量创建了一个和 days 数组相同的匿名数组,也有两个 int 类型的值:

    (int [2]){10, 20};  // 复合字面量
    

    去掉声明中的数组名,留下的 int [2] 即是复合字面量的类型名

    初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数:

    (int []){10, 20, 30}  // 内含 3 个元素的复合字面量
    

    因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它

    使用指针记录地址就是一种用法

    int * pt1;
    pt1 = (int [2]){10, 20};
    

    该复合字面量与上面创建的 days 数组的字面常量完全相同

    复合字面量的类型名也代表首元素的地址,*pt1 是 10,pt1[1] 是 20

    还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数:

    int sum(const int ar[], int n);
    ...
    int total;
    total = sum((int []){4, 4, 4, 5, 5, 5}, 6);
    

    第 1 个实参是内含 6 个 int 类型值的数组,和数组名类似,这同时也是该数组首元素的地址。这种用法的好处是,把信息传入函数前不必先创建数组,这是复合字面量的典型用法

    可以把这种用法用于二维数组或多维数组

    int (*pt2)[4];  // 声明一个指向二维数组的指针,该数组内含 2 个数组元素,每个元素是内含 4 个 int 类型值的数组
    pt2  = (int [2][4]){{1, 2, 3, -9}, {4, 5, 6, -8}};
    

    该字面量的类型是 int [2][4],即一个 2x4 的 int 数组

    复合字面量是提供只临时需要的值的一种手段

    复合字面量具有块作用域,这意味着一旦离开定义复合字面量的块,程序将无法保证该字面量是否存在。也就是说,复合字面量的定义在最内层的花括号中

  • 相关阅读:
    Java-Maven(三):Maven坐标、Maven仓库、Maven生命周期
    Java-Maven(二):Maven常用命令
    CF796C Bank Hacking题解
    米特运输——(dfs)
    lower_bound()和upper_bound()如果搜下标到头返回啥
    骑士https://vjudge.net/contest/364177#problem/Y——(基环树)
    CF1215DTicketGame——(博弈)
    #define xhxj (Xin Hang senior sister(学姐))
    The Meaningless Game 题解
    P2827 蚯蚓
  • 原文地址:https://www.cnblogs.com/sch01ar/p/9142685.html
Copyright © 2020-2023  润新知