C Primer Plus 笔记
简介
存一些零碎知识点
开的有点迟,前面的没记
逗号表达式
1  | int x = (2,3);  | 
7
else if 的本质
实际上,else if 是己学过的 if else 语句的变式。
1  | if (kwh <= BREAK1)  | 
其与如下代码等价
1  | if (kwh <= BREAK1)  | 
else 与 if 的配对规则
如果没有花括号,else 与离它最近的 if 匹配,除非最近的 if 被花括号括起来
1  | if (number >6)  | 
如果number的值是 5,程序将没有输出。
因为例中的 else 是和第二个 if 匹配的,改变一下缩进可能会更加明显。
1  | if (number >6)  | 
条件运算符
1  | x = (y<0) ?-y:y;  | 
switch 语句
case 下记得放个 break, 否则会匹配后面所有 case
goto 语句
一般用于跳出多层循环
9
return 语句
C 中,return 语句无法返回两个及以上的值,必要可使用指针进行操作
间接运算符
声明指针
1  | int * ptr = #  | 
求值(解引用)
其实就是取指针指向的地址的内容
1  | val = *ptr;  | 
10
未指定长度的不可变数组
1  | const int array[] = {1,2,3,4,5,6,7};  | 
指定初始化容器(C99)
1  | 
  | 
1  | // 稍作变化  | 
这种方法可以用于初始化指定位置的元素,而初始化其之前的元素
注意到,第一个例子中,对索引为1的元素进行了重复赋值,编译器选择使用第二次的值覆盖了第一次的值
如果这种初始化方法用于未指定长度的数组会怎么样?
1  | int array[] = {1,[1] = 2,3,4,5,6};  | 
编译器会创建一个能装得下初始化数值的数组,即长度为6的数组
数组下标越界的后果
1  | // bounds.c -- exceed the bounds of an array  | 
注意,value2和arr[-1]的内存地址相同,value1和arr[6]的内存地址相同,而arr的正常索引范围应该是0-3
在对数组进行循环赋值后,value2的值发生了变化
这意味着,在修改数组元素的值时,如果下标越界,会修改数组之外的内存地址的值,可能是某个变量,也可能是其他东西
VLA(C99)
C99 之前,声明数组的时候方括号里的数字,也就是数组的长度,只能使用整型常量表达式,且运算结果大于 0
1  | int test[5];  | 
C99 中,允许变量表示数组长度
1  | int n = 8;  | 
这就是 VLA (Variable-length-array),可变长度数组
VLA 必须是自动储存类别,这意味着它无法使用static,extern等储存类别说明符修饰
另外,在作为形参声明时,变量必须先于数组
1  | int func(int ar[col][row], int col, int row); // 无效  | 
在函数原型中,可以如下声明
1  | int func(int, int, int[*][*]); // 省略变量名后,使用星号替代  | 
二维数组
初始化
1  | 
  | 
1  | 
  | 
1  | // output:  | 
同样地,如果某个数组未初始化完全,剩下的值会被设为 0
如果某个数组初始化时元素数量超过了数组长度,不会影响其他数组的初始化
二维数组的”内层越界”
二维数组看作是一种特殊的一维数组
1  | 
  | 
猜猜输出是多少?是2234
数组的第一行在初始化的时候输入了 4 个元素,但是因为每行长度为2,所以后面两个元素被舍弃
最后的数组应该是这样
1  | {  | 
test[0][2],想想似乎是越界的,因为每个数组索引最大是1
但是 C 语言的二维数组储存是一维线性顺序储存的,并且按照行优先原则,
1  | 二维数组A[m][n]按行优先存储的线性序列为:  | 
这么来说,
对于二维数组A[m][n],可以理解为长度为m*n的一维数组
A[i][j]等效于一维下的A[i*m+j],但是写A[2]并不会返回2234,而是返回一个垃圾值
如果按照上一行的推理,[0][2]就相当于[1][0],因为0*0+2=1*2+0所以上例的输出是2234
回味一下,是不是明白了什么?
数组与指针
数组名其实是一个常量指针
1  | int main(void)  | 
数组名是该数组首个元素的地址
类似的,下面两个表达式均为真
1  | dates + 2 == &dates[2]; // 地址==地址  | 
因为是常量指针,所以任何对数组名的赋值操作都是非法的
但是你可以创建一个同类型指针来替代它
1  | 
  | 
指针+1 在数组中的体现
1  | // pnt_add.c -- pointer addition  | 
你可能会疑问,上面的地址运算结果不对, 61FE00 +1 应该是61FE01
但其实 C 中的指针+1,是指增加一个储存单元,对于数组而言,就是下一个元素的地址,而不是下一个字节的地址
这也是为什么指针要声明类型,因为每种类型的长度不同。如例中short为 2,double为 8
C 对数组的定义
C 标准在描述数组表示法时借用了指针的概念
ar[n] == *(ar+n)
*(ar + n)可以理解为,到内存的 ar 位置,移动 n 个单元后,取当前位置值。
*为间接运算符,此处起解引用作用
如何在函数形参表示数组
数组名其实是数组中首个元素的地址,所以在传入数组的时候,其实传入的是一个地址
所以可以在函数定义中这么写
1  | int func(int *array);  | 
第二种不但表示指针ar指向int类型,还是一个int类型的数组,但int ar[]这种写法只能在声明形参时使用
由于函数原型可以省略参数名,你还可以在函数原型中这么写
1  | int func(int *);  | 
不要在函数内求数组大小
1  | int main(void)  | 
两次的输出不一样,为什么?
当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针
对于main函数来说,array 是一个数组,可以直接用 sizeof 求出其”长度”,5 个 int 嘛,所以是 20
对于array_max函数来说,array 是一个整型指针,用 sizeof 求 array 大小,就相当于sizeof(int*),在我的电脑上永远是 8
指针操作
赋值
把地址赋给指针,可使用数组名,&,另一个指针等
解引用
*ptr取得指针所指向地址上的值
取址
指针变量也有自己的值和地址
指针的值即为指向的地址
指针的值可以通过&取得
递增(减)
ptr++  ptr--
递增指向数组元素的指针可以让指针指向下一个元素,或者上一个元素
与整数相加(减)
ptr + 1  ,指向下一个元素
整数会和指针指向的类型的大小(字节为单位)相乘,然后结果与地址相加(减去),差不多就是多次递增(或者递减)
指针求差
求出两个指针之间的差值
通常来说,两个指针分别指向同一个数组的不同元素,差值的单位与数组类型的单位相同
例如,
1  | int ar[4] = {1,2,3,4};  | 
如果对两指针求差,ptr2 - ptr1 得到的值将会是2,是指两个指针所指向的元素相隔两个int,而不是 2 字节
Const 指针
对于一个const指针,它可以指向const或非const数据,但是无法通过指针对数据进行任何修改
对于非const指针,它只能指向非const数据,否则通过指针就能改变数据(注意,的确可以通过这种方式修改 const 数据,但是这违背了的初衷)
指针和多维数组
给出一个二维数组
1  | int zippo[4][2]=  | 
有下列等式成立
1  | zippo == &zippo[0]  | 
因为数组名是其首元素的地址,所以前两条很容易得到
那么在加上解引用符号*,就可以得到下面的两个式子。
解引用两次才能得到原始值,这被称为双重间接
指向多维数组的指针
1  | int (*pz)[2];  | 
考虑一下,有什么区别?
pz指向一个内含两个 int 类型值的数组,是一个指针
pax是一个内含两个指针的数组,每个元素都是指向 int 类型型的指针
为什么?
由于()的存在,pz 首先与*结合,因此声明的是一个指向数组(内含两个 int 型元素)的指针
由于[]优先级更高,先与 pax 结合,从而声明了一个数组,随后*又表示数组内的元素是指针
举个例子
1  | 
  | 
例中声明一个p指针,指向一个包含 5 个元素的数组,并在初始化时指向二维数组中的第一个数组(数组名就是首元素地址)
此时p值为{1,2,3,4,5}的地址
第一次解引用的对象是指针p,也就是{1,2,3,4,5}的地址,得到了其值(1`的地址)
第二次解引用的对象指针{1,2,3,4,5},也就是是1的地址,得到了其值,即为1
一般而言,指向多维数组的指针如下进行声明
1  | 
  | 
如果是作为函数的形参,则可以这么写
1  | int func( int [][2][3][4][5] ); // 需要传入一个指向五维数的指针  | 
复合字面量(C99)
不就是匿名数组吗
字面量是啥? 简单说,5是一个 int 字面量,5.56是一个 double 字面量,'y'是一个 char 字面量…
复合字面量有点像数组,把很多同类字面量放到一起
1  | int arr[2] = {1,2}; //平时的数组  | 
因为复合字面量是匿名的,所以必须在创建的时候就使用它,指针是一种方法
1  | 
  | 
注意,复合字面量也具有类似数组名的特性,p在这里指向了第一个元素2323的地址。
11
字符串字面量
用双引号括起来的内容称为字符串字面量(string literal),也叫作字符串常量(string constant)
1  | const char *ptr = "Hello!" //等号右边就是一个字符串字面量  | 
通常,字符串都作为可执行文件的一部分储存在数据段中
当把程序载入内存时,也载入了程序中的字符串
一般来说,字符串字面量被视为 const 数据。这意味着你不能修改字符串字面量,这是一种未定义行为。
用两行代码来举例
1  | char * word = "frame";  | 
第一行的写法并不被 ISO C 所推荐
ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]
这也强调了,字符串字面量不应被修改
改为如下写法即可解决
1  | char const * word = "frame";  | 
但即使这样,第二行有效吗?
各个编译器有自己的说法,C primer plus 的作者运行环境下允许如此,而我自己的 Mingw gcc 下编译运行后直接Segmentation fault
所以,不要动字符串字面量,至少不应该
字符串字面量和字符串数组
1  | const char *mytalents[5] = {  | 
mytalents是一个包含了 5 个指针元素的一维数组,大小为 5*8=40
yourtalents是一个包含了 5 个数组(字符串数组)的二维数组,每个子数组大小为 10×4=40,总大小为 5×10×4 = 200
mytalents中的指针指向字符串字面量的地址,而yourtalents中储存的是字符串字面量的副本,也就是说每个字符串被储存了两次
C Primer Plus 笔记