Introduction
《C语言内核深度解析》的读书笔记
ISBN:978-7-115-43294-0
主要内容系摘录。
1. C语言与内存
32位的系统中可用内存是小于等于4G的,这里涉及了三总线的概念:地址总线、数据总线和控制总线。比如我们现在要向内存中写入一个数据,控制总线上传输写指令,地址总线上传输内存地址,而数据总线则传输要写入内存的数据。
我们说的多少位CPU,指的是数据总线的位数。数据线越多,一次处理的数据就越多,性能就越好。
内存位宽(内存数据线的数量)是指在一个时钟周期内所能传输数据的位数。8位内存模型表示一次可以传送的数据为8bit。
内存编址是以字节为单位,一个地址对应一个字节。
内存对齐。一般高级语言编写程序时,内存空间分配会自动对齐。比如32位内存模型,一行是4个byte,定义一个int,在内存中分配4个字节,有两种分配的方式:
- 1 2 3 4 对齐访问
- 1 2 3 4 或 2 3 4 5 等 非对齐访问
函数名的本质也是一个内存地址。(函数指针)
类型只是对其所修饰的数字或者符号所代表的内存空间的长度和解析方法的规定
int a[5] = {1, 2, 3, 4, 5};
a+1表示的是a[1]。
结构体变量在定义后,编译器在编译时会为所有成员分配空间。向函数传递结构时,实际上是传递的结构成员的值,即都是值传递方式(包括用结构体变量作为函数参数以及函数返回值也是结构体变量的情况)。由此可以看出,结构体变量名代表的是整个结构体变量,而不像数组名代表地址。所以一般都是传递结构体指针。
结构体中的函数指针类似与class中的成员方法。面向对象是一种思想,而不是某种类型的语言,因此C也可以写面向对象的程序。
1.7 栈
一定不要从北调函数返回一个局部变量的地址给主调函数,因为在函数执行完后,局部变量就释放了,这个地址里面的内容可能被新的内容填充。
c语言中定义局部变量不能定义太大或太多。
1.8 堆
堆管理内存的特点:
- 容量不限,动态分配
- 申请和释放都需要手工进行malloc、free
realloc可以改变原来申请的空间的大小。
calloc会将返回的内存初始化为0。
非静态局部变量存储在栈(stack)中,静态局部变量和全局变量存储在静态存储区,动态申请数据存在于堆(heap)中。编译器在编译程序时就确定了静态存储区的大小,静态存储区随着程序运行分配空间,直到程序结束才释放内存空间。
第一章的Tips
- 没有操作系统时(裸机程序),程序员需要直接操作内存,自己计算内存的使用和安排。
2. C语言位操作
CPU对寄存器的读写一般是按照寄存器的数据宽度一起读写,即32bit读出,32bit写入。
特定位清零用REG1 &= 0xFFFF0FFF
,置1用REG1 |= 0x0000FF00
。
3. 指针
指针变量的类型作用:
- 程序员写代码时识别用:不涉及强制转换时,知道该变量中应该存放什么类型的数值
- 空间大小的说明:比如int为4字节空间
- 存储结构说明:float和int虽然都是4bytes,但其存储结构完全不同
- 星号的个数表明了指针变量的级数
某类型一级指针变量 = 该类型一级地址
某类型二级指针变量 = &(该类型一级指针变量)
n+1级指针变量 = &(n级指针变量)
所有普通变量的地址都是一级地址,所有一级指针变量的地址都是二级地址。
使用指针的目的是为了更加方便的访问空间,但是如果级数超过3,实际上不但降低了程序的可读性,也会降低对空间的访问速度。
3.3 理解指针符号
3.3.1 星号
- 表明指针变量的级数
- 解引用,*p表示p所指向的空间。
3.3.2 取地址符号&
取变量空间的首地址
int a;
int *p = &a;
&a
是一个不可分割的整体,a有四个字节,取第一个字节的地址给p。编译时,会将&a
变成a空间的地址赋值给p。
3.4 野指针
野指针:指针指向一个不确定的地址空间,或者虽然指向一个确定的地址空间,但是引用空间的结果却是不可预知的。
其产生的原因:
- 使用指针前,忘记将其初始化或者赋值为一个有效地址空间
- 不清楚某些地址空间的访问权限。
int *p = "hello"; *(p+1) = 'w';
- 访问空间是,内存越界。
NULL到底是什么?
#ifdef _cplusplus
#define NULL 0
#else
#define NULL (void*)0
#endif
C++中,NULL指针可以是整数类型0,不会做严格的类型检查,但是在C语言中不行。
3.5 const
int const *p
等价与const int *p
,表示p所指向的空间是常量,不能被修改,但是p本身可以被修改。
int a = 10;
int b = 20;
int const *p = &a;
*p = 100; //编译时会报错,因为p指向的空间不能被修改
p = &b; //没有问题,p本身可以被修改
int * const p
表示指针变量p不能被修改,但是p指向的空间的内容可以被修改。
int const * const p
p的指向和所指向空间的内容都不能被修改。
const的变量可以间接被修改:
int const a=10;
int *p = (int *) &a;
*p = 100;//运行后a=100了
所以,只要变量的地址存在被引用的可能,const修饰的变量是可以被间接修改的。
3.8 指针类型与类型强制转换
指针本身强制类型转换:
int a;
int *pa = &a;
float *pb = NULL;
pb - (float *) pa;
pb与pa里面的地址值是相同的,但是pa存放的是(int ),pb放的是(float )型。使用*pb去使用变量空间a时,会以float型的空间大小和数据存储结构使用a空间的值。
需要注意以下问题:
引用空间大小发生变化
float a = 13.5;
double *pa = (double)&a;
double b = *pa;
*pa = 345.45;
本例中a的空间只有4个字节,但是pa却以double的方式指向了空间a,通过*pa去读取a的值13.5时,除了会读a空间的4个字节以外,还会读a空间后面紧跟的4个字节。如果是写入新数据,则这8个字节也会被修改,引起严重的错误。所以对于指针做强制转换,一般只会将引用空间缩小。
数据存储结构变化
int a = 125;
float b;
float *pa = NULL;
pa = (float *)&a;
float b = *pa;
printf("%f\n", b);//打印结果是乱码
3.9 sizeof
sizeof(str)
包含\0
字符,但是strlen(str)
则不会包含。
void fun(int n, int buf[n])
如果buf要写n,则n必须定义在buf前。
#define
和typedef
都可以用来给现有类型起别名,但是define只是简单的宏替换,而typedef不是。define在预编译时被处理,typedef在编译时被处理。
区别1
#define dpchar char*;
typedef char * tpchar;
dpchar p1, p2;//等价于char *p1,p2; p2是char,不是*
tpchar p1,p2;//两个都是指针。
区别2
#define dint int;
typedef int tint;
unsigned dInt p1,p2;
unsigned tInt p1,p2;//不可以。
区别3
typedef可以组建新类型,define不行。
typedef char[200] charBuf;
charBuf buf;
3.10 指针与函数传参
传参方式只有一种,都是传递值,而我们平时认为的两种方式(传递值、传递指针)实际是一种误解。只不过传指针时,被传值比较特殊,是一个地址。
C语言中值传递的本质是:当调用被调函数时,被调用函数会在自己的函数栈中开辟相同类型的形参空间,并且将传递过来的值写入形参空间保存。
比如
fun(int *p1, int *p2)//在fun函数栈中开出p1、p2指针变量空间,存放传递过来的地址
3.11 输入型参数和输出型参数
当函数需要多个返回值时,可以采用形参来实现,只是此时需要传递指针:
fun(int a,int b,int *d,int *e){
*d = a * a;
*e = b * b;
}
int main(void){
int d1,e1;
fun(1, 2, &d1, &e1);
return 0;
}
当使用传参来实现函数的返回值时,函数真正的返回值,绝大多数情况下,都是通过返回0和-1来表明函数执行成功还是失败。
4. C语言复杂表达式与指针高级应用
4.2 数组指针与指针数组
- 数组指针:实质是一个指针,指向了一个数组。
- 指针数组:实质是一个数组,里面的元素都是指针变量。
对于int *p[5];
,运算符优先级-百度百科中表明了[]
比*
优先级高,因此p先与[]
结合,表明p是一个数组,然后再与*
结合,表明数组元素是指针。因此整个符号是一个指针数组。
4.3 函数指针与typedef
函数名和数组名最大的区别就是:函数名做右值时加不加&
效果是一样的,但是数组名做右值时加不加&
意义就不一样。
C语言中类型一共有两种:
- 编译器定义的原生类型(基础数据类型,如int、double之类)
- 用户自定义类型(数组类型、结构体类型、函数类型等)
typedef是给类型重命名。
4.4 函数指针实战1
我们在输入内容时,都以\n
结尾,但是程序中scanf的时候都不会去收最后的\n
,导致这个回车符还留在标准输入中,下次再scanf就会被先拿出来,很多scanf的错误就是这么来的。
Linux中命令行默认是行缓冲的。
4.6 再论typedef
4.6.1 理解
typedef int (*fun_ptr)(int ,int)
:
- typedef是给类型取别名,所以只要是typedef定义的东西都是类型,所以fun_ptr是个类型。
- 去掉typedef,再将typedef定义的类型看成变量,就能明白该表达式是想定义一个函数指针类型。
- typedef在语法上是一个存储类关键字(如auto、extern、static、register),而变量只能被一种存储类的关键字修饰,否则编译报错。
4.6.2 与define
define只是单纯替换,末尾没有分号,如果有,则分号作为第二个参数的一部分也会替换。
Comments