C Primer Plus 笔记

简介

存一些零碎知识点

开的有点迟,前面的没记

逗号表达式

1
2
3
4
int x = (2,3);
int y = 2,3;
printf("%d %d",x,y);
// x=3 y=2

7

else if 的本质

实际上,else if 是己学过的 if else 语句的变式。

1
2
3
4
5
6
7
8
if (kwh <= BREAK1)
bill = RATE1 * kwh;
else if (kwh <= BREAK2) // kwh between 360 and 468
bill = BASE1 + (RATE2 * (kwh - BREAK1));
else if (kwh <= BREAK3) // kwh betweent 468 and 720
bill = BASE2 + (RATE3 * (kwh - BREAK2));
else // kwh above 680
bill = BASE3 + (RATE4 * (kwh - BREAK3));

其与如下代码等价

1
2
3
4
5
6
7
8
9
10
if (kwh <= BREAK1)
bill = RATE1 * kwh;
else
if (kwh <= BREAK2) // 360~468 kwh
bill = BASE1 + (RATE2 * (kwh - BREAK1));
else
if (kwh <= BREAK3) // 468~720 kwh
bill = BASE2 + (RATE3 * (kwh - BREAK2));
else // 超过 720 kwh
bill = BASE3 + (RATE4 * (kwh - BREAK3));

else 与 if 的配对规则

如果没有花括号,else 与离它最近的 if 匹配,除非最近的 if 被花括号括起来

1
2
3
4
5
if (number >6)
if (number <12)
printf("Case a\n");
else
printf("Case b\n");

如果number的值是 5,程序将没有输出。

因为例中的 else 是和第二个 if 匹配的,改变一下缩进可能会更加明显。

1
2
3
4
5
if (number >6)
if (number <12)
printf("Case a\n");
else
printf("Case b\n");

条件运算符

1
x = (y<0) ?-y:y;

switch 语句

case 下记得放个 break, 否则会匹配后面所有 case

goto 语句

一般用于跳出多层循环

9

return 语句

C 中,return 语句无法返回两个及以上的值,必要可使用指针进行操作

间接运算符

声明指针

1
int * ptr = &num;

求值(解引用)

其实就是取指针指向的地址的内容

1
val = *ptr;

10

未指定长度的不可变数组

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

指定初始化容器(C99)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int days[MONTHS] = {31,28, [4] = 31,30,31, [1] = 29};
int i;

for (i = 0; i < MONTHS; i++)
printf("%2d %d\n", i + 1, days[i]);

return 0;
}
// output:
// 1 31
// 2 29
// 3 0
// 4 0
// 5 31
// 6 30
// 7 31
// 8 0
// 9 0
// 10 0
// 11 0
// 12 0
1
2
3
4
5
6
7
8
9
10
11
// 稍作变化
#define MONTHS 7
int days[MONTHS] = {[4] = 31,30,31};
// output:
// 1 0
// 2 0
// 3 0
// 4 0
// 5 31
// 6 30
// 7 31

这种方法可以用于初始化指定位置的元素,而初始化其之前的元素

注意到,第一个例子中,对索引为1的元素进行了重复赋值,编译器选择使用第二次的值覆盖了第一次的值

如果这种初始化方法用于未指定长度的数组会怎么样?

1
int array[] = {1,[1] = 2,3,4,5,6};

编译器会创建一个能装得下初始化数值的数组,即长度为6的数组

数组下标越界的后果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// bounds.c -- exceed the bounds of an array
#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\n", value1, value2);
for (i = -1; i <= SIZE; i++)
arr[i] = 2 * i + 1;

for (i = -1; i < 7; i++)
printf("%2d %d\n", i , arr[i]);
printf("value1 = %d, value2 = %d\n", value1, value2);

printf("address of arr[-1]: %p\n", &arr[-1]);
printf("address of value1: %p\n", &value1);
printf("address of value2: %p\n", &value2);
printf("address of arr[6]: %p\n", &arr[6]);

return 0;

}

// output:
// value1 = 44, value2 = 88
// -1 -1
// 0 1
// 1 3
// 2 5
// 3 7
// 4 9
// 5 0
// 6 44
// value1 = 44, value2 = -1
// address of arr[-1]: 000000000061FDFC
// address of value1: 000000000061FE18
// address of value2: 000000000061FDFC
// address of arr[6]: 000000000061FE18

注意,value2arr[-1]的内存地址相同,value1arr[6]的内存地址相同,而arr的正常索引范围应该是0-3

在对数组进行循环赋值后,value2的值发生了变化

这意味着,在修改数组元素的值时,如果下标越界,会修改数组之外的内存地址的值,可能是某个变量,也可能是其他东西

VLA(C99)

C99 之前,声明数组的时候方括号里的数字,也就是数组的长度,只能使用整型常量表达式,且运算结果大于 0

1
2
3
4
int test[5];
int arr[(int)2.5];
int arr[2*5];
int arr[sizeof(test)];

C99 中,允许变量表示数组长度

1
2
3
4
int n = 8;
const int m = 8;
int arr[n];
int arr[m]; // C99/C11允许

这就是 VLA (Variable-length-array),可变长度数组

VLA 必须是自动储存类别,这意味着它无法使用static,extern等储存类别说明符修饰

另外,在作为形参声明时,变量必须先于数组

1
2
int func(int ar[col][row], int col, int row); // 无效
int func(int col, int row, int ar[col][row]); // 合法

函数原型中,可以如下声明

1
int func(int, int, int[*][*]); // 省略变量名后,使用星号替代

二维数组

初始化

1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
int test[2][2] = {0,1,2,3}; // 更真实地反应内部储存方式
printf("The first number is %d\n",test[0][0]);
printf("The last number is %d",test[1][1]);
}
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(void)
{
int test[2][2] = // 更加的直观
{
{0,1},
{2,3}
};
printf("The first number is %d\n",test[0][0]);
printf("The last number is %d",test[1][1]);
}
1
2
3
// output:
// The first number is 0
// The last number is 3

同样地,如果某个数组未初始化完全,剩下的值会被设为 0

如果某个数组初始化时元素数量超过了数组长度,不会影响其他数组的初始化

二维数组的”内层越界”

二维数组看作是一种特殊的一维数组

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main(void)
{
int test[2][2] =
{
{0,1,5,6},
{2234,3}
};
printf("The number is %d\n",test[0][2]);
}

猜猜输出是多少?是2234

数组的第一行在初始化的时候输入了 4 个元素,但是因为每行长度为2,所以后面两个元素被舍弃

最后的数组应该是这样

1
2
3
4
{
{0,1},
{2234,3}
}

test[0][2],想想似乎是越界的,因为每个数组索引最大是1

但是 C 语言的二维数组储存是一维线性顺序储存的,并且按照行优先原则,

1
2
二维数组A[m][n]按行优先存储的线性序列为:
A[0][0]、A[1][0]…A[m][0]、A[0][1]、A[1][1]…A[m][1]…A[m][1]、A[0][n]…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
2
3
4
5
6
7
8
9
10
11
12
13
int main(void)
{
int test[2] ={2345,1,2,34};
if (test == &test[0])
{
printf("%d %d\n",*test,test[0]);
printf("%p %p",test,&test[0]);
}

}
// output:
// 2345 2345
// 000000000061FE18 000000000061FE18

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

类似的,下面两个表达式均为真

1
2
dates + 2 == &dates[2]; // 地址==地址
*(dates + 2) == dates[2];// 值==值

因为是常量指针,所以任何对数组名的赋值操作都是非法的

但是你可以创建一个同类型指针来替代它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main(void)
{
int sum;
int test[4] ={200,1,2,34};
int *p = test;
while (p < &test[4])
{
sum += *p;
p++;
}
printf("%d",sum);
}
/*
output:
237
*/

指针+1 在数组中的体现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// pnt_add.c -- pointer addition
#include <stdio.h>
#define SIZE 4
int main(void)
{
short dates [SIZE];
short * pti;
short index;
double bills[SIZE];
double * ptf;

pti = dates; // assign address of array to pointer
ptf = bills;
printf("%23s %15s\n", "short", "double");
for (index = 0; index < SIZE; index ++)
printf("pointers + %d: %10p %10p\n",
index, pti + index, ptf + index);

return 0;
}
// output:
/*
short double
pointers + 0: 000000000061FE00 000000000061FDE0
pointers + 1: 000000000061FE02 000000000061FDE8
pointers + 2: 000000000061FE04 000000000061FDF0
pointers + 3: 000000000061FE06 000000000061FDF8
*/

你可能会疑问,上面的地址运算结果不对, 61FE00 +1 应该是61FE01

但其实 C 中的指针+1,是指增加一个储存单元,对于数组而言,就是下一个元素的地址,而不是下一个字节的地址

这也是为什么指针要声明类型,因为每种类型的长度不同。如例中short为 2,double为 8

C 对数组的定义

C 标准在描述数组表示法时借用了指针的概念

ar[n] == *(ar+n)

*(ar + n)可以理解为,到内存的 ar 位置,移动 n 个单元后,取当前位置值。

*为间接运算符,此处起解引用作用

如何在函数形参表示数组

数组名其实是数组中首个元素的地址,所以在传入数组的时候,其实传入的是一个地址

所以可以在函数定义中这么写

1
2
int func(int *array);
int func(int ar[]);

第二种不但表示指针ar指向int类型,还是一个int类型的数组,但int ar[]这种写法只能在声明形参时使用

由于函数原型可以省略参数名,你还可以在函数原型中这么写

1
2
int func(int *);
int func(int []);

不要在函数内求数组大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(void)
{
int array[5] = {1,2,3,4,5};
printf("%d %d\n",sizeof(array),sizeof(array[0]));
}
void array_max(int array[])
{
printf("%d %d",sizeof(array),sizeof(array[0]));
printf(" %d",sizeof(int*));
}
/*
20 4
8 4 8
*/

两次的输出不一样,为什么?

当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针

对于main函数来说,array 是一个数组,可以直接用 sizeof 求出其”长度”,5 个 int 嘛,所以是 20

对于array_max函数来说,array 是一个整型指针,用 sizeof 求 array 大小,就相当于sizeof(int*),在我的电脑上永远是 8

指针操作

赋值

把地址赋给指针,可使用数组名&,另一个指针

解引用

*ptr取得指针所指向地址上的值

取址

指针变量也有自己的值和地址

指针的值即为指向的地址

指针的值可以通过&取得

递增(减)

ptr++ ptr--

递增指向数组元素的指针可以让指针指向下一个元素,或者上一个元素

与整数相加(减)

ptr + 1 ,指向下一个元素

整数会和指针指向的类型的大小(字节为单位)相乘,然后结果与地址相加(减去),差不多就是多次递增(或者递减)

指针求差

求出两个指针之间的差值

通常来说,两个指针分别指向同一个数组的不同元素,差值的单位与数组类型的单位相同

例如,

1
2
3
int ar[4] = {1,2,3,4};
int *ptr1 = &ar[0];
int *ptr2 = &ar[2];

如果对两指针求差,ptr2 - ptr1 得到的值将会是2,是指两个指针所指向的元素相隔两个int,而不是 2 字节

Const 指针

对于一个const指针,它可以指向const非const数据,但是无法通过指针对数据进行任何修改

对于非const指针,它只能指向非const数据,否则通过指针就能改变数据(注意,的确可以通过这种方式修改 const 数据,但是这违背了的初衷)

指针和多维数组

给出一个二维数组

1
2
3
4
5
6
7
int zippo[4][2]=
{
{1,3},
{2,3},
{2342,34},
{242,234}
};

有下列等式成立

1
2
3
4
5
zippo == &zippo[0]
zippo[0] == &zippo[0][0]

*zippo == &zippo[0][0]
**zippo == zippo[0][0]

因为数组名是其首元素的地址,所以前两条很容易得到

那么在加上解引用符号*,就可以得到下面的两个式子。

解引用两次才能得到原始值,这被称为双重间接

指向多维数组的指针

1
2
int (*pz)[2];
int * pax[2];

考虑一下,有什么区别?

pz指向一个内含两个 int 类型值的数组,是一个指针

pax是一个内含两个指针的数组,每个元素都是指向 int 类型型的指针

为什么?

由于()的存在,pz 首先与*结合,因此声明的是一个指向数组(内含两个 int 型元素)的指针

由于[]优先级更高,先与 pax 结合,从而声明了一个数组,随后*又表示数组内的元素是指针

举个例子

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main(void)
{
int a[3][5] =
{
{1, 2, 3, 4, 5},
{6, 7, 8, 9, 10},
{11, 12, 13, 14, 15}};
int (*p)[5] = a;
printf("%d",**p);
}

例中声明一个p指针,指向一个包含 5 个元素的数组,并在初始化时指向二维数组中的第一个数组(数组名就是首元素地址)

此时p值为{1,2,3,4,5}的地址

第一次解引用的对象是指针p,也就是{1,2,3,4,5}的地址,得到了其值(1`的地址)

第二次解引用的对象指针{1,2,3,4,5},也就是是1的地址,得到了其值,即为1

一般而言,指向多维数组的指针如下进行声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main(void)
{
int array[1][2][3][4][5];
array[0][0][0][0][0] = 1234;
array[0][0][0][0][1] = 12345;

int(*p1)[2][3][4][5] = array; // 指向五维数组 array
int(*p2)[3][4][5] = array[0]; // 指向四维数组 array[0]
int(*p3)[4][5] = array[0][0]; // 指向三维数组 array[0][0]
int(*p4)[5] = array[0][0][0]; // 指向二维数组 array[0][0][0]
int(*p5) = array[0][0][0][0]; // 指向一维数组 array[0][0][0][0]
printf("%d %d %d %d %d\n",*****(p1),****p2,***p3,**p4,*p5);
printf("%d %d %d %d %d",*(****p1+1),*(***p2+1),*(**p3+1),*(*p4+1),*(p5+1));
/*
1234 1234 1234 1234 1234
12345 12345 12345 12345 12345
*/
}

如果是作为函数的形参,则可以这么写

1
int func( int [][2][3][4][5] ); // 需要传入一个指向五维数的指针

复合字面量(C99)

不就是匿名数组吗

字面量是啥? 简单说,5是一个 int 字面量,5.56是一个 double 字面量,'y'是一个 char 字面量…

复合字面量有点像数组,把很多同类字面量放到一起

1
2
int arr[2] = {1,2}; //平时的数组
(int [2] ) {1,2}; //复合字面量

因为复合字面量是匿名的,所以必须在创建的时候就使用它,指针是一种方法

1
2
3
4
5
6
#include <stdio.h>
int main(void)
{
int *p = (int[2]){2323, 2};
printf("%d", *p);
}

注意,复合字面量也具有类似数组名的特性,p在这里指向了第一个元素2323的地址。

11

字符串字面量

用双引号括起来的内容称为字符串字面量(string literal),也叫作字符串常量(string constant)

1
const char *ptr = "Hello!" //等号右边就是一个字符串字面量

通常,字符串都作为可执行文件的一部分储存在数据段中

当把程序载入内存时,也载入了程序中的字符串

一般来说,字符串字面量被视为 const 数据。这意味着你不能修改字符串字面量,这是一种未定义行为。

用两行代码来举例

1
2
char * word = "frame";
word[1] = 'l';

第一行的写法并不被 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    const char *mytalents[5] = {
"Adding numbers swiftly",
"Multiplying accurately", "Stashing data",
"Following instructions to the letter",
"Understanding the C language"
};
const char yourtalents[5][10] = {
"Walking in a straight line",
"Sleeping", "Watching television",
"Mailing letters", "Reading email"
};
printf("\nsizeof mytalents: %zd, sizeof yourtalents: %zd\n",
sizeof(mytalents), sizeof(yourtalents))
/*
sizeof mytalents: 40, sizeof yourtalents: 200
*/

mytalents是一个包含了 5 个指针元素的一维数组,大小为 5*8=40

yourtalents是一个包含了 5 个数组(字符串数组)的二维数组,每个子数组大小为 10×4=40,总大小为 5×10×4 = 200

mytalents中的指针指向字符串字面量的地址,而yourtalents中储存的是字符串字面量的副本,也就是说每个字符串被储存了两次

Author

BakaFT

Posted on

2020-07-19

Updated on

2023-12-28

Licensed under

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×