第一章、数据的存储

1.整型在内存中的存储

1.1 原码、反码和补码

1
int a = 10;

计算机中的整数有三种表示方法,即原码、反码和补码。

三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位负整数的三种表示方法各不相同

原码—直接将二进制按照正负数的形式翻译成二进制就可以。

反码—将原码的符号位不变,其他位依次按位取反就可以得到了。

补码—反码+1即为补码

数值位正整数的原码、反码、补码都相同

对于整型来说,数据存放内存中其实存放的是补码。

原因:在计算机系统中,数值一律用补码来表示和存储。原因在于使用补码可以将符号位和数值域统 一处理;

同时,加法和减法也可以统一处理(CPU只有加法器),另一方面,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

1.2 大端和小端

1
2
3
4
5
6
7
int a = -10;
//原码:10000000 00000000 00000000 00001010
//反码:11111111 11111111 11111111 11110101
//补码:11111111 11111111 11111111 11110110
// f f f f f f f 6
//0x ff ff ff f6
//但是在vs中,a的地址却是f6 ff ff ff

大小端是什么:

大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;

小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。

为什么会有大端和小端:

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。

但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),另外,对于位数大于8位 的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。

例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。

对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式则刚好相反。

我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

举一个例题:

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。

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
#include <stdio.h>
int check_sys(){
int i = 1;
return (*(char *)&i);
}
int main(){
int ret = check_sys();
if(ret == 1){
printf("小端\n");
}
else{
printf("大端\n");
}
return 0;
}

//代码2
int check_sys(){
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}

2.浮点型在内存中的存储

2.1 浮点数存储的例子

1
2
3
4
5
6
7
8
9
10
11
12
int main(){
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);

*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);

return 0;
}

输出结果:

n的值为:9

*pFloat的值为:0.000000

num的值为:1091567616

*pFloat的值为:9.000000

2.2 浮点数存储规则

num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?

要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。

详细解读:

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:

(-1)^S * M * 2^E

(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。

M表示有效数字,大于等于1,小于2。

2^E表示指数位。

举例:

十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。

那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。

十进制的-5.0,写成二进制是-101.0 ,相当于-1.01×2^2 。

那么,s=1,M=1.01,E=2。

IEEE 754规定:

对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。

对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。

IEEE 754对有效数字M和指数E,还有一些特别规定:

前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。

比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的是为了节省1位有效数字。

以32位浮点数为例,留给M只有23位, 将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂:

首先,E为一个无符号整数(unsigned int)。

这意味着,如果E为8位,它的取值范围为0255;如果E为11位,它的取值范围为02047。

但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。

比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。

然后,指数E从内存中取出还可以再分成三种情况:

①E不全为0或不全为1

这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。

比如:

0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为 1.0*2^(-1),其阶码为-1+127=126,表示为 01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:

1
0 01111110 00000000000000000000000 

②E全为0

这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

③E全为1

这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);

解释一开始的例子:

为什么 0x00000009 还原成浮点数,就成了 0.000000 ?

首先,将 0x00000009 拆分,得到第一位符号位s=0,后面8位的指数 E=00000000 ,最后23位的有效数字M=000 0000 0000 0000 0000 1001。

1
9 -> 0000 0000 0000 0000 0000 0000 0000 1001

由于指数E全为0,所以符合上一节的第二种情况。因此,浮点数V就写成:

V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)

显然,V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。

浮点数9.0,如何用二进制表示?还原成十进制又是多少?

首先,浮点数9.0等于二进制的1001.0,即1.001×2^3

1
9.0 -> 1001.0 ->(-1)^01.0012^3 -> s=0, M=1.001,E=3+127=130

那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130, 即10000010。所以,写成二进制形式,应该是s+E+M,即

1
0 10000010 001 0000 0000 0000 0000 0000 

这个32位的二进制数,还原成十进制,正是1091567616 。

第二章、指针的进阶

1.字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char* ;

注意:

1
const char* pstr = "hello";

这里并不是把字符串 hello 放到字符指针 pstr 里了,本质是把字符串 hello 首字符的地址放到了 pstr 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int main() {
char str1[] = "hello";
char str2[] = "hello";
const char *str3 = "hello";
const char *str4 = "hello";
   if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");

if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");

return 0;
}

//str1 and str2 are not same
//str3 and str4 are same

这里str3和str4指向的是一个同一个常量字符串。

C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。

所以str1和str2不同,str3和str4相同。

2.指针数组

指针数组是一个存放指针的数组。

1
2
3
int* arr1[10]; //整形指针的数组
char* arr2[10]; //一级字符指针的数组
char** arr3[10];//二级字符指针的数组

3.数组指针

3.1 定义

数组指针是指针,是能够指向数组的指针。

1
2
3
4
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。

//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。

3.2 “数组名” vs “&数组名”

1
int arr[10];

arr 和 &arr 是一样的东西吗?

举个例子:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);

printf("&arr= %p\n", &arr);
printf("&arr+1= %p\n", &arr+1);
return 0;
}

结果:

arr = 012FF6EC

arr+1 = 012FF6F0

&arr= 012FF6EC

&arr+1= 012FF714

从上面代码的结果可知:&arr和arr,有同样的数值,但本质上是不同的。

实际上:

arr是数组名,数组名表示数组首元素的地址。

&arr 表示的是数组的地址,而不是数组首元素的地址。

本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型

数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40.

4.函数指针

1
2
3
4
5
6
7
8
9
#include <stdio.h>
void test() {
printf("hello\n");
}
int main() {
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}

结果:

003A1474

003A1474

输出了两个地址,这两个地址是 test 函数的地址。那么,如何保存函数的地址呢?

1
2
3
4
void test() {
printf("hehe\n");
}
void (*pfun)(); //pfun是指针,指向一个函数

补充两段有趣的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//代码1
( *( void (*)() )0 )();
//把0强制转换成无参无返回值的函数指针类型,然后将0解引用后调用
//就是调用0地址处的函数

//代码2
void ( *signal( int , void(*)(int) ) )(int);
//signal是一个函数声明
//这个函数参数有2个,一个是int,另一个是函数指针,该指针指向的函数参数为int,返回类型是void
//signal函数的返回类型也是函数指针,该指针指向的函数参数int,返回类型是void
//简化版本
typedef void(* pfun_t)(int);
pfun_t signal(int,pfun_t);

5.函数指针数组

数组是一个存放相同类型数据的存储空间,指针数组是一个存放指针的数组。

那如果把函数的地址存到一个数组中,这个数组就叫函数指针数组。

1
int (*parr[10])();

函数指针数组的用途:转移表

实例:

使用函数指针数组实现计算器。

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
#include <stdio.h>
int add(int a,int b){
return a+b;
}
int sub(int a,int b){
return a-b;
}
int mul(int a,int b){
return a*b;
}
int div(int a,int b){
return a/b;
}

int main(){
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表    
while (input){
printf( "*************************\n" );
printf( " 1:add           2:sub \n" );
printf( " 3:mul           4:div \n" );
printf( "*************************\n" );
printf( "请选择:" );
scanf( "%d", &input);
if ((input <= 4 && input >= 1)){
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = (*p[input])(x, y);
}else{
printf( "输入有误\n" );
}
printf( "ret = %d\n", ret);
}
return 0;
}

6.指向函数指针数组的指针

指向函数指针数组的指针是一个指针

指针指向一个数组 ,数组的元素都是函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void test(const char* str){
printf("%s\n", str);
}
int main(){
//函数指针pfun
void (*pfun)(const char*) = test;

//函数指针的数组pfunArr
void (*pfunArr[5])(const char* str);
pfunArr[0] = test;

//指向函数指针数组pfunArr的指针ppfunArr
void (*(*ppfunArr)[5])(const char*) = &pfunArr;

return 0;
}

7.回调函数

回调函数就是一个通过函数指针调用的函数。

如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

举个例子:

首先先演示一下qsort函数(快速排序)的使用:

qsot函数可以排序任意类型的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
//qosrt函数的使用者须实现一个比较函数
//void*是一种无具体类型的指针
//void*的指针变量可以存放任意类型的地址
//void*的指针不能直接进行解引用、 +-*/ 操作
int int_cmp(const void * p1, const void * p2){
return (*( int *)p1 - *(int *) p2);
}
int main(){
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++){
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}

使用回调函数,模拟实现qsort(采用冒泡的方式)

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
#include <stdio.h>
//比较函数
int int_cmp(const void * p1, const void * p2){
return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size){
int i = 0;
for (i = 0; i< size; i++){
char tmp = *((char *)p1 + i);
*(( char *)p1 + i) = *((char *) p2 + i);
*(( char *)p2 + i) = tmp;
}
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *)){
int i = 0;
int j = 0;
for (i = 0; i< count - 1; i++){
for (j = 0; j<count-i-1; j++){
if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0){
_swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
}
}
}
}
int main(){
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++){
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}

8.指针和数组的笔试题

X86环境

(X86环境下地址大小为4,X64环境下地址大小为8)

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
//一维数组
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a)); //16(数组名a单独放在sizeof内部,计算整个数组的大小)
printf("%d\n",sizeof(a+0)); //4 (a表示首元素的地址,a+0同样表示首元素的地址)
printf("%d\n",sizeof(*a)); //4 (*a表示对首元素地址的解引用)
printf("%d\n",sizeof(a+1)); //4 (a表示首元素的地址,a+1表示第二个元素的地址)
printf("%d\n",sizeof(a[1])); //4 (a[1]是数组的第二个元素)
printf("%d\n",sizeof(&a)); //4 (&a表示数组的地址)
printf("%d\n",sizeof(*&a)); //16(*&a表示对数组的地址的解引用)
printf("%d\n",sizeof(&a+1)); //4 (&a+1跳过整个数组后的地址)
printf("%d\n",sizeof(&a[0])); //4 (&a[0]表示数组第一个元素的地址)
printf("%d\n",sizeof(&a[0]+1)); //4 (&a[0]+1表示数组第二个元素的地址)

//字符数组
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr)); //6
printf("%d\n", sizeof(arr+0)); //4
printf("%d\n", sizeof(*arr)); //1
printf("%d\n", sizeof(arr[1])); //1
printf("%d\n", sizeof(&arr)); //4
printf("%d\n", sizeof(&arr+1)); //4
printf("%d\n", sizeof(&arr[0]+1)); //4

printf("%d\n", strlen(arr)); //随机值,因为末尾没有'\0'
printf("%d\n", strlen(arr+0)); //随机值,因为末尾没有'\0'
printf("%d\n", strlen(*arr)); //报错,strlen会把97('a'的ascii值)作为起始地址统计字符串,内存访问冲突
printf("%d\n", strlen(arr[1])); //报错,同上
printf("%d\n", strlen(&arr)); //报错,&arr是arr数组的地址,与形参参数 类型不符合
printf("%d\n", strlen(&arr+1)); //报错,同上
printf("%d\n", strlen(&arr[0]+1)); //随机值,因为末尾没有'\0'
//sizeof是操作符,任意类型均可
//sizeof获取了数据在内存中所占用的存储空间,以字节为单位来计数
//strlen是库函数,针对字符串
//strlen关注字符串中'\0'的位置,计算的是'\0'前出现了多少个字符

char arr[] = "abcdef";
printf("%d\n", sizeof(arr)); //7
printf("%d\n", sizeof(arr+0)); //4
printf("%d\n", sizeof(*arr)); //1
printf("%d\n", sizeof(arr[1])); //1
printf("%d\n", sizeof(&arr)); //4
printf("%d\n", sizeof(&arr+1)); //4
printf("%d\n", sizeof(&arr[0]+1)); //4

printf("%d\n", strlen(arr)); //6
printf("%d\n", strlen(arr+0)); //6
printf("%d\n", strlen(*arr)); //报错,strlen会把97('a'的ascii值)作为起始地址统计字符串,内存访问冲突
printf("%d\n", strlen(arr[1])); //报错,同上
printf("%d\n", strlen(&arr)); //报错,&arr是arr数组的地址,与形参参数 类型不符合
printf("%d\n", strlen(&arr+1)); //报错,同上
printf("%d\n", strlen(&arr[0]+1)); //5

char *p = "abcdef";
printf("%d\n", sizeof(p)); //4
printf("%d\n", sizeof(p+1)); //4
printf("%d\n", sizeof(*p)); //1
printf("%d\n", sizeof(p[0])); //1
printf("%d\n", sizeof(&p)); //4
printf("%d\n", sizeof(&p+1)); //4
printf("%d\n", sizeof(&p[0]+1)); //4

printf("%d\n", strlen(p)); //6
printf("%d\n", strlen(p+1)); //5
printf("%d\n", strlen(*p)); //报错
printf("%d\n", strlen(p[0])); //报错
printf("%d\n", strlen(&p)); //报错
printf("%d\n", strlen(&p+1)); //报错
printf("%d\n", strlen(&p[0]+1)); //5

//二维数组
int a[3][4] = {0};
printf("%d\n",sizeof(a)); //48
printf("%d\n",sizeof(a[0][0])); //4
printf("%d\n",sizeof(a[0])); //16
printf("%d\n",sizeof(a[0]+1)); //4
printf("%d\n",sizeof(*(a[0]+1))); //4
printf("%d\n",sizeof(a+1)); //4
printf("%d\n",sizeof(*(a+1))); //16
printf("%d\n",sizeof(&a[0]+1)); //4
printf("%d\n",sizeof(*(&a[0]+1))); //16
printf("%d\n",sizeof(*a)); //16
printf("%d\n",sizeof(a[3])); //16

总结
数组名意义

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

9.指针笔试题

笔试题1

1
2
3
4
5
6
7
int main(){
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));
return 0;
}
//2,5

笔试题2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Test{
int Num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;

int main(){
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}
//00000014
//00000001
//00000004

笔试题3

1
2
3
4
5
6
7
8
int main(){
int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);
return 0;
}
//4,2000000

笔试题4

1
2
3
4
5
6
7
8
int main(){
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0];
printf("%d", p[0]);
return 0;
}
//1

笔试题5

1
2
3
4
5
6
7
8
int main(){
int a[5][5];
int(*p)[4];
p = a;
printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
//FFFFFFFC,-4

笔试题6

1
2
3
4
5
6
7
8
int main(){
int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int *ptr1 = (int *)(&aa + 1);
int *ptr2 = (int *)(*(aa + 1));
printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
//10,5

笔试题7

1
2
3
4
5
6
7
8
int main(){
char *a[] = {"work","at","alibaba"};
char**pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}
//at

笔试题8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(){
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *--*++cpp+3);
printf("%s\n", *cpp[-2]+3);
printf("%s\n", cpp[-1][-1]+1);
return 0;
}
//POINT
//ER
//ST
//EW

10.附录(qsort的使用)

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include<string.h>
#include<stdio.h>
#include<stdlib.h>//qsort的头文件

struct Student{
int id;
char name[50];
};

int int_cmp(const void* a, const void* b){
return *(int*)a - *(int*)b;
}
int char_cmp(const void* a, const void* b){
return *(char*)a - *(char*)b;
}
int double_cmp(const void* a, const void* b){
return *(double*)a > *(double*)b ? 1 : -1;
}
int struct_cmp(const void* a, const void* b) {
return ((struct Student*)a)->id - ((struct Student*)b)->id;
}
int str_cmp(const void* a, const void* b) {
return strcmp(*(char**)a, *(char**)b);
}

int main() {
//int排序
int num[5] = { 1,3,9,5,2 };
qsort(num, sizeof(num) / sizeof(num[0]), sizeof(num[0]), int_cmp);
for (int i = 0; i < sizeof(num) / sizeof(num[0]); i++) {
printf("%d ", num[i]);
}
printf("\n");
//char排序
char word[5] = "CADB";
qsort(word, sizeof(word) / sizeof(word[0]), sizeof(word[0]), char_cmp);
for (int i = 0; i < sizeof(word) / sizeof(word[0]); i++) {
printf("%c ", word[i]);
}
printf("\n");
//double排序
double in[5] = { 1.23,3.14159,2.6,5.74,4.11 };
qsort(in, sizeof(in) / sizeof(in[0]), sizeof(in[0]), double_cmp);
for (int i = 0; i < sizeof(in) / sizeof(in[0]); i++) {
printf("%lf ", in[i]);
}
printf("\n");
//struct排序
struct Student stu[3] = { {2,"小明"},{1,"小李"},{3,"小红"} };
qsort(stu, sizeof(stu) / sizeof(stu[0]), sizeof(stu[0]), struct_cmp);
for (int i = 0; i < sizeof(stu) / sizeof(stu[0]); i++) {
printf("%d%s ", stu[i].id,stu[i].name);
}
printf("\n");
//string排序
char *str[5] = { "AAA","EEE","CCC","BBB","DDD" };
qsort(str, sizeof(str) / sizeof(str[0]), sizeof(str[0]), str_cmp);
for (int i = 0; i < sizeof(str) / sizeof(str[0]); i++) {
printf("%s ", str[i]);
}
printf("\n");

return 0;
}

第三章、字符串+内存函数

C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串或者字符数组中。
字符串常量适用于那些对它不做修改的字符串函数.

1.函数介绍

1.1 strlen

1
size_t strlen ( const char * str );
  • strlen函数返回的是字符串中’\0’前面出现的字符个数(不包含’\0’)

  • strlen函数的返回值为size_t,是无符号的

size_t的真实类型与操作系统有关,

在32位架构中被普遍定义为:typedef unsigned int size_t;//4字节

而在64位架构中被定义为:typedef unsigned long size_t;//8字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
const char* str1 = "abcdef";
const char* str2 = "bbb";
if(strlen(str2)-strlen(str1)>0){
printf("str2>str1\n");
}
else{
printf("srt1>str2\n");
}
return 0;
}
//str2>str1
//因为strlen函数返回值为size_t,strlen(str2)-strlen(str1)的结果也是size_t,所以输出str2>str1

1.2 strcpy

字符串复制

1
char* strcpy(char * dest, const char * src );
  • Copies the C string pointed by source into the array pointed by destination, including the terminating null character (and stopping at that point).

  • 源字符串必须以 ‘\0’ 结束。

  • 会将源字符串中的 ‘\0’ 拷贝到目标空间。

  • 目标空间必须足够大,以确保能存放源字符串。

  • 目标空间必须可变

1.3 strcat

字符串连接

1
char * strcat ( char * dest, const char * src );
  • Appends a copy of the source string to the destination string. The terminating null character in destination is overwritten by the first character of source, and a null-character is included at the end of the new string formed by the concatenation of both in destination.

  • 源字符串必须以 ‘\0’ 结束。

  • 目标空间必须有足够的大,能容纳下源字符串的内容。

  • 目标空间必须可修改。

1.4 strcmp

字符串比较

1
int strcmp ( const char * str1, const char * str2 );
  • This function starts comparing the first character of each string. If they are equal to each other, it continues with the following pairs until the characters differ or until a terminating null-character is reached.

  • 第一个字符串大于第二个字符串,则返回大于0的数字;

  • 第一个字符串等于第二个字符串,则返回0;

  • 第一个字符串小于第二个字符串,则返回小于0的数字。

1.5 strncpy

将指定长度的字符串复制到字符数组中

1
char * strncpy ( char * destination, const char * source, size_t num );
  • Copies the first num characters of source to destination. If the end of the source C string (which is signaled by a null-character) is found before num characters have been copied, destination is padded with zeros until a total of num characters have been written to it.

  • 拷贝num个字符从源字符串到目标空间。

  • 如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

1.6 strncat

在字符串的结尾追加n个字符

1
char * strncat ( char * destination, const char * source, size_t num )
  • Appends the first num characters of source to destination, plus a terminating null-character.

  • If the length of the C string in source is less than num, only the content up to the terminating null-character is copied.

1.7 strncmp

比较字符串前 n 个字节

1
int strncmp ( const char * str1, const char * str2, size_t num );
  • 比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完 。

  • return value indicates
    <0 the first character that does not match has a lower value in str1 than in str2
    0 the contents of both strings are equal
    >0 the first character that does not match has a greater value in str1 than in str2

1.8 strstr

返回字符串中首次出现子串的地址

1
char * strstr ( const char *str1, const char * str2);
  • Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1.

1.9 strtok

分解字符串

1
char * strtok ( char * str, const char * sep );
  • sep参数是个字符串,定义了用作分隔符的字符集合。

  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。

  • strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)

  • strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。

  • strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。

  • 如果字符串中不存在更多的标记,则返回 NULL 指针。

1.10 strerror

返回一个指向错误字符串的指针

1
char * strerror ( int errnum );
  • 返回错误码,所对应的错误信息。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main (){
FILE *fp;
fp = fopen("file.txt","r");
if( fp == NULL ) {
printf("Error: %s\n", strerror(errno));
}
return(0);
}
//让我们编译并运行上面的程序,这将产生以下结果,因为我们尝试打开一个不存在的文件:
//Error: No such file or directory

1.11 memcpy

内存拷贝函数,从源内存地址的起始位置开始拷贝若干个字节到目标内存地址

1
void * memcpy ( void * destination, const void * source, size_t num );
  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 这个函数在遇到 ‘\0’ 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。
  • c语言规定,memcpy支持不重叠拷贝就够了,但是在vs,memcpy也能支持重叠拷贝

1.12 memmove

拷贝字节

1
void * memmove ( void * destination, const void * source, size_t num );
  • 和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  • 如果源空间和目标空间出现重叠,就得使用memmove函数处理。

1.13 memcmp

把存储区 ptr1 和存储区 ptr2 的前 n 个字节进行比较

1
2
3
int memcmp ( const void * ptr1,
const void * ptr2,
size_t num );
  • 比较从ptr1和ptr2指针开始的num个字节。

  • return value indicates
    <0 the first byte that does not match in both memory blocks has a lower value in ptr1 than in ptr2 (if evaluated as unsigned char values)
    0 the contents of both memory blocks are equal
    >0 the first byte that does not match in both memory blocks has a greater value in ptr1 than in ptr2 (if evaluated as unsigned char values)

2.函数的模拟实现

2.1 模拟实现strlen

三种方式

方式1(计数器)

1
2
3
4
5
6
7
8
int my_strlen(const char *str){
int count = 0;
while(*str){
count++;
str++;
}
return count;
}

方式2(无临时变量的计数器,递归)

1
2
3
4
5
6
7
int my_strlen(const char *str){
if(*str=='\0'){
return 0;
}else{
return 1+my_strlen(str+1);
}
}

方式3(指针-指针)

1
2
3
4
5
6
7
int my_strlen(const char *str){
char *p = s;
while(*p != '\0'){
p++;
}
return p-s;
}

2.2 模拟实现strcpy

1
2
3
4
5
6
char *strcpy(char *dest, const char *src ){
char *ret = dest;
assert(dest && src);
while(*dest++ = *src++);
return ret;
}

2.3 模拟实现strcat

1
2
3
4
5
6
7
8
9
char* strcat ( char *dest, const char *src ){
char *ret = dest;
assert(dest && src);
while(*dest){
dest++;
}
while(*dest++ = *src++);
return ret;
}

2.4 模拟实现strstr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char* strstr ( const char *str1, const char *str2){
char *cp = (char *)str1;
char *s1,*s2;
if(!*str2){
return ((char *)str1);
}
while(*cp){
s1 = cp;
s2 = (char *)str2;
while( *s1 && *s2 && !(*s1-*s2) ){
s1++;
s2++;
}
if(!*str2){
return ((char *)str1);
}
cp++;
}
return NULL;
}

2.5 模拟实现strcmp

1
2
3
4
5
6
7
8
9
10
11
12
13
int my_strcmp (const char * src, const char * dst){
int ret = 0 ;
assert(dst && src);
while( ! (ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst){
src++;
dst++;
}
if ( ret < 0 )
ret = -1 ;
else if ( ret > 0 )
ret = 1 ;
return ret;
}

2.6 模拟实现memcpy

1
2
3
4
5
6
7
8
9
10
void * memcpy ( void * dst, const void * src, size_t count){
void * ret = dst;
assert(dst && src);
while (count--) {
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
return ret;
}

2.7 模拟实现memmove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void * memmove ( void * dst, const void * src, size_t count){
void * ret = dst;
if (dst <= src || (char *)dst >= ((char *)src + count)) {
while (count--) {
*(char *)dst = *(char *)src;
dst = (char *)dst + 1;
src = (char *)src + 1;
}
}else{
dst = (char *)dst + count - 1;
src = (char *)src + count - 1;
while (count--) {
*(char *)dst = *(char *)src;
dst = (char *)dst - 1;
src = (char *)src - 1;
}
}
return ret;
}

第四章、自定义类型详解

1.结构体

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

1.1 结构的声明

1
2
3
4
5
6
7
//创建一个描述学生的结构体
struct Stu{
char name[20]; //名字
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
}; //分号不能丢

在声明结构的时候,可以不完全的声明(省略结构体的标签)。

1
2
3
4
5
struct{
int a;
char b;
float c;
}x;

1.2 结构的自引用

1
2
3
4
struct Node{
int data;
struct Node* next;
};

1.3 结构体变量的定义与初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Point{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};

struct Stu{
char name[15]; //名字
int age; //年龄
};
struct Stu s = {"zhangsan", 20}; //初始化

struct Node{
int data;
struct Point p;
struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

1.4 结构体内存对齐

结构体中元素是按照定义顺序一个一个放到内存中去的,但并不是紧密排列的。

结构体的对齐规则

1.第一个成员在与结构体变量偏移量为0的地址处。

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

(对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值)

3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么存在内存对齐?

大部份资料都是这样说的

1.平台原因(移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.性能原因:

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。

原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问

总体来说,结构体的内存对齐是拿空间来换取时间的做法。

所以我们在设计结构体时,应该让占用空间小的成员尽量集中在一起

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
//练习1
struct S1{
char c1;
int i;
char c2;
};
printf("%d\n", sizeof(struct S1));
//12

//练习2
struct S2{
char c1;
char c2;
int i;
};
printf("%d\n", sizeof(struct S2));
//8

//练习3
struct S3{
double d;
char c;
int i;
};
printf("%d\n", sizeof(struct S3));
//16

//练习4-结构体嵌套问题
struct S4{
char c1;
struct S3 s3;
double d;
};
printf("%d\n", sizeof(struct S4));
//32

1.5 修改默认对齐数

不知道大家是否见过 #pragma 这个预处理指令,这里使用可以改变我们的默认对齐数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

#pragma pack(1)//设置默认对齐数为1
struct S2{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

int main(){
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}

结论:结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

1.6 结构体传参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct S{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};

//结构体传参
void print1(struct S s){
printf("%d\n", s.num);
}

//结构体地址传参
void print2(struct S* ps){
printf("%d\n", ps->num);
}

int main(){
print1(s); //传结构体
print2(&s); //传地址
return 0;
}

上面的 print1 和 print2 函数哪个好些?

答案:首选print2函数。

结构体传参的时候,要传结构体的地址。

原因:

函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。

如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

2.位段

2.1 什么是位段

位段的声明和结构是类似的,有两个不同:

1.位段的成员必须是 int、unsigned int 或signed int 。

2.位段的成员名后边有一个冒号和一个数字。

实例:

1
2
3
4
5
6
7
8
9
10
struct A{
//4byte
int _a:2; //剩30bit
int _b:5; //剩25bit
int _c:10; //剩15bit
//4byte(vs上不会使用之前剩余的15bit)
int _d:30;
};
//A就是一个位段类型
//位段A的大小 8字节

2.2 位段的内存分配

1.位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

2.位段的空间上是按照需要以**4个字节( int )或者1个字节( char )**的方式来开辟的。

3.位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段

2.3 位段的跨平台问题

1.int 位段被当成有符号数还是无符号数是不确定的。

2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

3.位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

4.当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

3.枚举

3.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
//星期
enum Day{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};

//性别
enum Sex{
MALE,
FEMALE,
SECRET
};

//颜色
enum Color{
RED,
GREEN,
BLUE
};

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。

{}中的内容是枚举类型的可能取值,也叫 枚举常量 。

这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。

例如:

1
2
3
4
5
6
//颜色
enum Color{
RED = 1,
GREEN = 2,
BLUE = 4
};

3.2 枚举的优点

1.增加代码的可读性和可维护性

2.和#define定义的标识符比较,枚举有类型检查,更加严谨。

3.防止了命名污染(封装)

4.便于调试

5.使用方便,一次可以定义多个常量

3.3 枚举的应用

1
2
3
4
5
6
enum Color{
RED=1,
GREEN=2,
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

4.联合(共用体)

4.1 联合类型的定义

联合也是一种特殊的自定义类型

这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。

1
2
3
4
5
6
7
//联合类型的声明
union Un{
char c;
int i;
};
//联合变量的定义
union Un un;

4.2 联合的特点

联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
union Un{
int i;
char c;
};
union Un un;

// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//结果是一样的(说明同一时间只能用一个)

//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
//

4.3 联合大小的计算

联合的大小至少是最大成员的大小。

当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

1
2
3
4
5
6
7
8
9
10
11
union Un1{
char c[5];
int i;
};
union Un2{
short c[7];
int i;
};
//下面输出的结果是什么?
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));

4.4 面试题

判断当前计算机的大小端存储

1
2
3
4
5
6
7
8
9
10
11
int check_sys(){
union
{
int i;
char c;
}un;
un.i = 1;
return un.c;
}
//如果返回1,表示小端
//如果返回0,表示大端

第五章、动态内存管理

1.动态内存分配的必要性

1
2
int val = 20;        //在栈空间上开辟四个字节
char arr[10] = {0}; //在栈空间上开辟10个字节的连续空间

上述开辟空间方式有两个特点:

1.空间开辟大小是固定的。

2.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态存开辟了

2.动态内存函数的介绍

2.1 malloc 和 free

malloc和free都声明在 stdlib.h 头文件中

1
void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。

  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

1
void free (void* ptr);

free函数用来释放动态开辟的内存。

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

2.2 calloc

1
void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

2.3 realloc

1
void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址

  • size 调整之后新大小

  • 返回值为调整之后的内存起始位置。

  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc调整内存空间的是存在两种情况

第一种情况:原有空间之后有足够大的空间

扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。

第二种情况:原有空间之后没有足够大的空间

在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址

realloc如果调整失败,会返回一个空指针

1
2
3
4
5
6
7
8
9
int* p=(int*)calloc(10,sizeof(int));
int* ptr=(int*)realloc(p,80);
if(NULL != ptr){//避免realloc调整失败后返回空指针覆盖p
p=ptr;
}
free(ptr);
ptr=NULL;
free(p);
p=NULL;

3.常见的动态内存错误

3.1 对NULL指针的解引用操作

1
2
3
4
5
void test(){
int *p = (int *)malloc(INT_MAX);
*p = 20; //如果p的值是NULL,就会有问题
free(p);
}

3.2 对动态开辟空间的越界访问

1
2
3
4
5
6
7
8
9
10
11
void test(){
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p){
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++){
*(p+i) = i; //当i是10的时候越界访问
}
free(p);
}

3.3 对非动态开辟内存使用free释放

1
2
3
4
5
void test(){
int a = 10;
int *p = &a;
free(p);//ok?
}

3.4 使用free释放一块动态开辟内存的一部分

1
2
3
4
5
void test(){
int *p = (int *)malloc(100);
p++;
free(p); //p不再指向动态内存的起始位置
}

3.5 对同一块动态内存多次释放

1
2
3
4
5
void test(){
int *p = (int *)malloc(100);
free(p);
free(p); //重复释放
}

3.6 动态开辟 内存忘记释放(内存泄漏)

1
2
3
4
5
6
7
8
9
10
11
12
void test(){
int *p = (int *)malloc(100);
if(NULL != p){
*p = 20;
}
//忘记释放,就会出现内存泄漏的问题
}

int main(){
test();
while(1);
}

动态开辟的空间一定要释放,并且正确释放。

4.经典的笔试题

题目1

1
2
3
4
5
6
7
8
9
void GetMemory(char *p){
p = (char *)malloc(100);
}
void Test(void){
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world"); //str仍然是空指针,会非法访问内存,程序会崩溃
printf(str);
}

请问运行Test 函数会有什么样的结果?

程序崩溃

1
2
3
4
5
6
7
8
9
10
11
12
//修改后
void GetMemory(char** p){
*p = (char *)malloc(100);
}
void Test(void){
char *str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}

题目2

1
2
3
4
5
6
7
8
9
char *GetMemory(void){
char p[] = "hello world";
return p;
}
void Test(void){
char *str = NULL;
str = GetMemory();
printf(str);
}

请问运行Test 函数会有什么样的结果?

会出现一段乱码,函数结束后空间会回收。

题目3

1
2
3
4
5
6
7
8
9
void GetMemory(char **p, int num){
*p = (char *)malloc(num);
}
void Test(void){
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}

请问运行Test 函数会有什么样的结果?

存在内存泄漏,没有释放内存。

题目4

1
2
3
4
5
6
7
8
9
void Test(void){
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str); //free不会让str指向空,需要主动把str指向NULL
if(str != NULL){
strcpy(str, "world");
printf(str);
}
}

请问运行Test 函数会有什么样的结果?

非法访问内存,str指向的空间释放后不能再次使用。

5.C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

1.内核空间:用户代码不能读写

2.栈区(stack)(向下增长):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

3.堆区(heap)(向上增长):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

4.数据段(静态区)(static)存放全局变量静态数据。程序结束后由系统释放。

5.代码段:存放函数体(类成员函数和全局函数)的二进制代码/只读常量

6.柔性数组

C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』(flexible array)成员。

1
2
3
4
typedef struct st_type{
int i;
int a[]; //柔性数组成员
}type_a;

6.1 柔性数组的特点

  • 结构中的柔性数组成员前面必须至少一个其他成员。

  • sizeof 返回的这种结构大小不包括柔性数组的内存。

  • 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如:

1
2
3
4
5
typedef struct st_type{
int i;
int a[]; //柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a)); //输出的是4

6.2 柔性数组的使用

1
2
3
4
5
6
7
8
9
//代码1
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//业务处理
p->i = 100;
for(i=0; i<100; i++){
p->a[i] = i;
}
free(p);

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

6.3 柔性数组的优势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//代码2
typedef struct st_type{
int i;
int *p_a;
}type_a;
type_a *p = (type_a *)malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));

//业务处理
for(i=0; i<100; i++){
p->p_a[i] = i;
}

//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;

上述 代码1 和 代码2 可以完成同样的功能,但是 方法1 的实现有两个好处:

  • 方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

  • 有利于提高访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

第六章、小练习:通讯录

一、介绍

通讯录(暂时不考虑名字相同)

1.可以存放1000个人的信息

2.人的信息:名字,年龄,电话,住址,性别

3.增、删、改、查联系人

动态增加

查询根据联系人的电话查询

4.排序(根据年龄排序)

二、完整代码

contact.h

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#define MAX 1000
#define NAME_MAX 20
#define SEX_MAX 5
#define ADDR_MAX 30
#define TELE_MAX 12
#define DEFAULE_SZ 10

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>

enum option {
EXIT,
ADD,
DEL,
SEARCH,
MODIFY,
SORT,
SHOW,
};

typedef struct PeoInfo {
char name[NAME_MAX];
int age;
char sex[SEX_MAX];
char addr[ADDR_MAX];
char tele[TELE_MAX];
}PeoInfo;

typedef struct Contact {
PeoInfo* data;//存放数据
int sz;//通讯录中有效信息的个数
int capacity;//通讯录的最大容量
}Contact;

//初始化通讯录
void InitContact(Contact* pc);

//销毁通讯录
void DestroyContact(Contact* pc);

//增加联系人
void AddContact(Contact* pc);

//打印所有通讯录信息
void ShowContact(const Contact* pc);

//打印一条通讯录信息
void ShowOneContact(const Contact* pc, int index);

//按电话查找
int FindByTele(const Contact* pc,char tele[]);

//删除联系人
void DeleteContact(Contact* pc);

//查找联系人
void SearchContact(Contact* pc);

//修改联系人
void ModifyContact(Contact* pc);

//联系人排序
void SortContact(Contact* pc);

contact.cpp

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include"contact.h"

void InitContact(Contact* pc) {
assert(pc);
pc->sz = 0;
PeoInfo* tmp = (PeoInfo*)malloc(DEFAULE_SZ * sizeof(PeoInfo));
if (tmp != NULL) {
pc->data = tmp;
}
else {
printf("InitContact()::%s\n",strerror(errno));
return;
}
pc->capacity = DEFAULE_SZ;
}

void check_capacity(Contact* pc) {
if (pc->sz == pc->capacity) {
//增加容量
PeoInfo* tmp = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo));
if (tmp != NULL) {
pc->data = tmp;
pc->capacity += 2;
printf("增容成功\n");
}
else {
printf("check_capacity()::%s\n", strerror(errno));
}
}
}

void DestroyContact(Contact* pc) {
free(pc->data);
pc->data = NULL;
pc->sz = 0;
pc->capacity = 0;
}

void AddContact(Contact* pc) {
assert(pc);
check_capacity(pc);

//输入联系人
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tele);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);

pc->sz++;
printf("增加联系人成功\n");
}

void ShowContact(const Contact* pc) {
assert(pc);
int i = 0;
printf("%-10s\t%-5s\t%-5s\t%-13s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
for (i = 0; i < pc->sz; i++) {
printf("%-10s\t%-5d\t%-5s\t%-13s\t%-20s\n",
pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
}
}

void ShowOneContact(const Contact* pc, int i) {
assert(pc);
printf("%-10s\t%-5s\t%-5s\t%-13s\t%-20s\n", "名字", "年龄", "性别", "电话", "地址");
printf("%-10s\t%-5d\t%-5s\t%-13s\t%-20s\n",
pc->data[i].name, pc->data[i].age, pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
}

int FindByTele(const Contact* pc,char tele[]){
int i = 0;
for (i = 0; i < pc->sz; i++) {
if (strcmp(pc->data[i].tele, tele) == 0) {
return i;
}
}
return -1;
}

void DeleteContact(Contact* pc) {
assert(pc);
char tele[TELE_MAX] = { 0 };
if (pc->sz == 0) {
printf("通讯录空了,无法删除\n");
return;
}
printf("请输入要删除人的电话:>");
scanf("%s", tele);
int pos = FindByTele(pc, tele);
if (pos == -1) {
printf("要删除的人不存在\n");
}
else {
int j = 0;
for (j = pos; j < pc->sz-1; j++) {
pc->data[j] = pc->data[j + 1];
}
pc->sz--;
printf("删除指定联系人成功\n");
}
}

void SearchContact(Contact* pc) {
assert(pc);
printf("请输入要查找人的电话:>");
char tele[TELE_MAX] = { 0 };
scanf("%s", tele);
int index = FindByTele(pc, tele);
if (index == -1) {
printf("通讯录里没有此人\n");
}
else {
printf("查找成功\n");
ShowOneContact(pc, index);
}
}

void ModifyContact(Contact* pc) {
assert(pc);
printf("请输入要修改人的电话:>");
char tele[TELE_MAX] = { 0 };
scanf("%s", tele);
int index = FindByTele(pc, tele);
if (index == -1) {
printf("要修改的人不存在\n");
}
else {
ShowOneContact(pc, index);
printf("请输入名字:>");
scanf("%s", pc->data[index].name);
printf("请输入年龄:>");
scanf("%d", &(pc->data[index].age));
printf("请输入性别:>");
scanf("%s", pc->data[index].sex);
printf("请输入电话:>");
scanf("%s", pc->data[index].tele);
printf("请输入地址:>");
scanf("%s", pc->data[index].addr);
printf("修改成功\n");
}
}

void SortContact(Contact* pc) {
int i = 0;
int j = 0;
for (i = 0; i < pc->sz; i++) {
for (j = 0; j < pc->sz - i - 1; j++) {
if (pc->data[j].age < pc->data[j + 1].age) {
PeoInfo temp = pc->data[j];
pc->data[j] = pc->data[j + 1];
pc->data[j + 1] = temp;
}
}
}
ShowContact(pc);
}

test.cpp

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
43
44
45
46
47
48
49
50
#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"

void menu() {
printf("**********************************************\n");
printf("******** 1.add 2.del ********\n");
printf("******** 3.search 4.modify ********\n");
printf("******** 5.sort 6.show ********\n");
printf("******** 0.exit ********\n");
printf("**********************************************\n");
}

int main() {
int input = 0;
Contact con = { 0 };
InitContact(&con);
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input) {
case ADD:
AddContact(&con);
break;
case DEL:
DeleteContact(&con);
break;
case SEARCH:
SearchContact(&con);
break;
case MODIFY:
ModifyContact(&con);
break;
case SORT:
SortContact(&con);
break;
case SHOW:
ShowContact(&con);
break;
case EXIT:
DestroyContact(&con);
printf("退出通讯录\n");
break;
default:
printf("选择错误!\n");
break;
}
} while (1);
return 0;
}

第七章、文件操作

1.文件的概念

磁盘上的文件是文件。

但是在程序设计中,我们一般谈的文件有两种:程序文件数据文件(从文件功能的角度来分类的)。

  • 程序文件

包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。

  • 数据文件

文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件 。

本节主要讨论的是数据文件。

一个文件要有一个唯一的文件标识,以便用户识别和引用。

文件名包含3部分:文件路径+文件名主干+文件后缀( 例如: c:\code\test.txt )

为了方便起见,文件标识常被称为文件名。

2.文件的打开和关闭

2.1 文件指针

缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。

每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.

例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:

1
2
3
4
5
6
7
8
9
10
11
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;

不同的c编译器的FILE类型包含的内容不完全相同,但是大同小异。

每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。

一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

1
FILE* pf; //文件指针变量

定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件

2.2 文件的打开和关闭

文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。

在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。

ANSIC 规定使用fopen函数来打开文件,fclose函数来关闭文件

1
2
3
4
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );

mode(文件使用方式):

文件使用方式 含义 如果指定文件不存在
“r”(只读) 为了输入数据,打开一个已经存在的文本文件 出错
“w”(只写) 为了输出数据,打开一个文本文件 建立一个新的文件
“a”(追加) 向文本文件尾添加数据 建立一个新的文件
“rb”(只读) 为了输入数据,打开一个二进制文件 出错
“wb”(只写) 为了输出数据,打开一个二进制文件 建立一个新的文件
“ab”(追加) 向一个二进制文件尾添加数据 出错
“r+”(读写) 为了读和写,打开一个文本文件 出错
“w+”(读写) 为了读和写,建议一个新的文件 建立一个新的文件
“a+”(读写) 打开一个文件,在文件尾进行读写 建立一个新的文件
“rb+”(读写) 为了读和写打开一个二进制文件 出错
“wb+”(读写) 为了读和写,新建一个新的二进制文件 建立一个新的文件
“ab+”(读写) 打开一个二进制文件,在文件尾进行读和写 建立一个新的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* fopen fclose example */
#include <stdio.h>
int main (){
FILE * pFile;
//打开文件
pFile = fopen ("myfile.txt","w");
//文件操作
if (pFile!=NULL){
fputs ("fopen example",pFile);
//关闭文件
fclose (pFile);
}
return 0;
}

3.文件的顺序读写

功能 函数名 适用于
字符输入函数 fgetc 所有输入流
字符输出函数 fputc 所有输出流
文本行输入函数 fgets 所有输入流
文本行输出函数 fputs 所有输出流
格式化输入函数 fscanf 所有输入流
格式化输出函数 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrite 文件

4.文件的随机读写

4.1 fseek

根据文件指针的位置和偏移量来定位文件指针。

1
int fseek ( FILE * stream, long int offset, int origin );

第一个参数stream为文件指针

第二个参数offset为偏移量,正数表示正向偏移,负数表示负向偏移

第三个参数origin设定从文件的哪里开始偏移,可能取值为:SEEK_CURSEEK_ENDSEEK_SET

SEEK_SET: 文件开头

SEEK_CUR: 当前位置

SEEK_END: 文件结尾

其中SEEK_SET,SEEK_CUR和SEEK_END依次为0,1和2。

1
2
3
4
5
6
7
8
9
10
11
12
/* fseek example */
#include <stdio.h>
int main (){
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple." , pFile );
fseek ( pFile , 9 , SEEK_SET );
fputs ( " sam" , pFile );
fclose ( pFile );
return 0;
}
//This is a sample.

4.2 ftell

返回文件指针相对于起始位置的偏移量。

1
long int ftell ( FILE * stream );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* ftell example */
/* getting size of a file */
#include <stdio.h>
int main (){
FILE * pFile;
long size;
pFile = fopen ("myfile.txt","rb");
if (pFile==NULL){
perror ("Error opening file");
}else{
fseek (pFile, 0, SEEK_END);
size=ftell (pFile);
fclose (pFile);
printf ("Size of myfile.txt: %ld bytes.\n",size);
}
return 0;
}

4.3 rewind

让文件指针的位置回到文件的起始位置

1
void rewind ( FILE * stream );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* rewind example */
#include <stdio.h>
int main (){
int n;
FILE * pFile;
char buffer [27];
pFile = fopen ("myfile.txt","w+");
for ( n='A' ; n<='Z' ; n++)
fputc ( n, pFile);
rewind (pFile);
fread (buffer,1,26,pFile);
fclose (pFile);
buffer[26]='\0';
puts (buffer);
return 0;
}

5.文本文件和二进制文件

根据数据的组织形式,数据文件被称为文本文件或者二进制文件

数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。

如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件

一个数据在内存中是怎么存储的呢?

字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。

如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。

6.文件读取结束的判定

被错误使用的feof

牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束

文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )

7.文件缓冲区

ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* example */
include <stdio.h>
#include <windows.h>
int main(){
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}

因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。

第八章、程序的编译

1.程序的翻译环境和执行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。

第2种是执行环境,它用于实际执行代码

2.详解编译+链接

2.1 翻译环境

组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。

每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。(linux后缀**.o** windows后缀**.obj**)

链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

翻译环境本身又分为几个阶段:

  • 预编译阶段

gcc -E test.c -o test.i

文本操作

①头文件的包含#include;

②删除注释

③#define定义符号的替换

  • 编译

gcc -S test.i

->test.s

把C语言转换成汇编代码

①语法分析

②词法分析

③语义分析

④符号汇总

推荐两本书《编译原理》,《程序员的自我修养》

  • 汇编

gcc -c test.s

->test.o

把汇编代码转换成二进制的指令

①形成符号表

  • 链接

gcc test.o xxx.o -o test

->test(可执行文件,如果不自定义的化,默认a.out)

生成可执行程序

①合并段表

②符号表的合并和符号表的重定位

多个目标文件进行链接的时候会通过符号表来查看来自外部的符号是否存在

2.2 运行环境

程序执行的过程:

①程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。

②程序的执行便开始。接着便调用main函数。

③开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

④终止程序。正常终止main函数;也有可能是意外终止。

3.预处理详解

3.1 预定义符号

1
2
3
4
5
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

这些预定义符号都是语言内置的,可以直接用。

1
2
/* example */
printf("file:%s line:%d\n", __FILE__, __LINE__);

3.2 #define

#define 定义标识符

1
2
3
4
5
6
7
8
9
10
/* example */
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

注意:在define定义标识符时,不要在后面加上 ;

#define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

宏的申明方式:

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中

1
2
/* example */
#define SQUARE(x) ((x) * (x))

注意:

参数列表的左括号必须与name紧邻。

如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

用于对数值表达式进行求值的宏定义都应该用加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

约定俗成:把宏名全部大写;函数名不要全部大写

#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

①在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。

②替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

③最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

①宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
②当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索 。

#和##

  • #的作用

①当字符串作为宏参数的时候可以把字符串放在字符串中

1
2
3
4
5
6
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE)
int main() {
PRINT("%d", 10);
return 0;
}

②使用 # ,可以把一个宏参数变成对应的字符串

1
2
3
4
5
6
#define PRINT(n) printf("the value of "#n" is %d\n",n)
int main(){
int a = 10;
PRINT(a);
return 0
}
  • ##的作用

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

1
2
3
4
5
6
7
8
#define ADD_TO_SUM(num, value) sum##num += value;

int main() {
int sum5 = 10;
ADD_TO_SUM(5, 10);//作用是:给sum5增加10.
printf("%d", sum5);
return 0;
}

带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

x+1;//不带副作用
x++;//带有副作用

1
2
3
4
5
6
7
8
9
10
//证明 具有副作用的参数所引起的问题 的例子
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main() {
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
//x=6 y=10 z=9

3.3 宏和函数的对比

宏通常被应用于执行简单的运算 ,为什么不用函数来执行呢?

原因:

①用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。

所以宏比函数在程序的规模和速度方面更胜一筹。

②更为重要的是函数的参数必须声明为特定的类型。

所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。

宏是类型无关的。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

1
2
3
4
5
6
7
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));

宏的缺点:当然和函数相比宏也有劣势的地方:

①每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

② 宏是没法调试的。

③宏由于类型无关,也就不够严谨。

④宏可能会带来运算符优先级的问题,导致程容易出现错。

宏和函数的对比

属性 #define定义宏 函数
代码长度 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括 号 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带有副作用的参数 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 函数参数只在传参的时候求值一 次,结果更容易控制。
参数类型 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型。 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的

3.3 #undef

这条指令用于移除一个宏定义

1
2
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

3.4 命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(){
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++){
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++){
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}

编译指令:

1
2
//linux 环境演示
gcc -D ARRAY_SIZE=10 programe.c

3.5 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

比如:

调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#define __DEBUG__
int main(){
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++){
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}

常见的条件编译指令

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
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

3.判断是否被定义
#if defined(symbol)
#ifdef symbol

#if !defined(symbol)
#ifndef symbol

4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

3.6 文件包含

#include 指令可以使另外一个文件被编译。预处理器会先删除这条指令,并用包含文件的内容替换。

  • 本地文件包含
1
#include "filename"

先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。

  • 库文件包含
1
#include <filename.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。 所以对于库文件也可以使用 “ ” 的形式包含(但是这样效率低,也不容易区分本地文件和库文件) 。

  • 嵌套文件包含

如果出现了嵌套文件包含,就容易造成文件内容的重复,为了解决这个问题,可以使用条件编译。

在每个头文件的开头写:

1
2
3
4
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__

或者

1
#pragma once

4.其他预处理指令

1
2
3
4
5
#error
#pragma
#line
...
//内容较多,不做具体介绍,有兴趣可以自己去了解

5.小练习

  • 写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换。
1
# define SWAP_ODD_EVEN(NUM) ((((NUM) & (0X55555555)) << 1) | (((NUM) & (0XAAAAAAAA)) >> 1))
  • 写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明
1
#define  STRUCT_OFFSET(id, element)  ((unsigned long)&((( struct id*)0)->element))

假设结构体的首地址是0开始,把他转换成结构体指针类型,再用 -> 获取他的成员,前面加 & 就是为了获取这个成员的地址。,最后再强制转换成unsigned long, 这样就得到了偏移量。