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 笔记