1、野指针:就是指针指向的位置是不可知的,指向的位置可能不能访问,触发段错误
2、怎么避免野指针:在指针解引用之前,要确保指针指向一个绝对可用的空间
常规做法:第一点:在定义指针时同时初始化为NULL;
第二点:在解引用之前先判断这个指针是不是NULL;
第三点:使用完后再将其赋值为NULL;
第四点:在使用指针前,先将其赋值绑定一个可用的地址空间。
3、NULL到底是什么
在C/C++中定义为:
#ifdef _cplusplus
#define NULL 0
#else
#define NULL (void *)0
#endif
为什么将指针指向0地址处?第一:0地址作为一个特殊地址(我们认为指针指向这个地址就表示指针没有被初始化,就表示野指针);第二:0地址在一般的操作系统中都是不可被访问的,如果不按规矩直接去解引用就会触发段错误,这已是最好的结果了。
4、const关键字,用来修饰变量,并表示这个变量是常量
第一种:const int *p;
第二种:int const *p;
第三种:int * const p;
第四种:const int * const p;
关于const修饰的变量的理解,主要涉及两个变量:第一个是指针变量p本身;第二个是p指向的那个变量的值(*p)
5、数组中a和&a不能做左值,因为a代表整体的数组空间,对数组操作要单独操作,不能整体操作;&a是一个编译器自动分配的好的内存地址,所以是个常量,不能做左值。
6、&a、a、&a[0]从数值上是相同的,但从意义上看,a和&a[0]是数组首元素的首地址,而&a是整个数组的首地址;从类型来看,a和&a[0]是元素指针,也就是int*类型的;而&a是数组指针,是int (*)[]类型的。这个区别对指针运算会产生影响。
7、指针变量+1时实际吧不是+1而是加1*sizeof(指针类型),主要原因是希望指针+1后刚好指向下一个元素。
(一个字节占8个bit(0x00),所以一个int类型的就占32个字节(0x0000))
8、函数形参是数组时,实际传的不是整个数组,而是数组的首元素首地址,实际上相当于传递的是指针,也就是占4个字节。所以传递数组大小时要另外将sizeof(a)传进函数中,表示数组大小。
9、在子函数内传参得到的数组首元素首地址和外面直接得到的数组首元素的首地址是相同的,这就叫做“传址调用”,此时可以通过穿进去的地址来访问实参。,数组作为函数形参时,[ ]里的数字可有可无,这是因为数组名做形参传递的只是个指针,根本就没有数组长度这个信息。
10、结构体变量作为函数形参的时候,实际和普通变量时表现一样。由于结构体一般比较大,所以也用指针来传参。
11、传值调用描述的是这样一种现象:x和y作为实参,自己并没有进入交换函数swap中,而只是在内存中拷贝了自己的副本进入子函数swap中,然后在子函数中进行交换,根本影响不了真正的x、y,所以只是在swap内部交换了,外部的根本没有受影响。
12、传址调用中x和y的真身仍然没有进入swap中,而是把x和y的地址传了进去,也就是说子函数中拷贝出来的指针变量中存的是x和y的地址,于是在子函数内通过指针解引用的方式从函数内部范围到x和y的真身,从而改变了x和y。
13、typedef可以同时定义变量类型和指针类型(注意:这里定义的类型)
typedef struct teacher
{
char name[20];
int age;
int mager;
}teacher, *pTeacher;
14、使用typedef与const的注意:
typedef int *PINT; const PINTp2; 相当于是int *const p2;
typedef int *PINT; PINT const p2; 相当于是int*const p2;
如果确实想得到const int* p,只能typedefconst int *CPINT; CPINT p1;
15、int *p1[5];int **p2;指针数组和二重指针匹配(p1=p2;)p1做右值表示数组首元素的首地址,数组的元素就是int*类型的,所以p1做右值就表示一个int*类型地址,所以p1就是一个指向int类型变量的指针的指针,所以它是一个二重指针int**。
16、int a[1][2];int (*p3)[5];数组指针和二重数组匹配,a是首元素首地址,这个地址里存的就是二维数组中的第一维这个数组,和数组指针类型的意义相同。
17、inta[1][2];
int *p=a[0]; 这样是匹配的,a[0]是二维数组的第一维数组,这里就代表数组名,也就是第一维数组的首地址,等同于 &a[0][0],所以这两个完全匹配。
18、栈的特点:运行时自动分配&回收、反复使用、脏内存、临时性(所以函数不能返回栈指针,因为这个空间是临时的,访问到地方也会随时会改变,没有意义),栈会溢出(局部变量太多或进行递归调用)。
18、堆内存特点:操作系统堆管理器管理(分配灵活、按需分配)、大块内存、程序手动申请&释放、脏内存、临时性(只在malloc和free之间可以访问)。
19、堆内存使用步骤:申请并绑定、检验是否申请成功、释放。
20、如果在free之前给p另外赋值,那么malloc申请的那段内存就丢失,也就是说操作系统的堆管理器中这段内存在当前进程中使用,但你也用不了了,所以你就必须申请新的内存来使用,知道系统中内存用尽,这就叫做程序“吃内存”(内存泄露)。
21、C语言中使用char*p="linux";定义字符串时,这个字符串实际被分配在代码段,也就是说这个"linux"字符串是一个常量而不是变量字符串,所以就不能被改变。
22、const型常量:gcc中const修饰的常量还是放在数据段,所一可以用指针来改变。
23、char*p="linux"中,p是一个字符指针,占4个字节;"linux"分配在代码段,占6个字节,这10个字节中4个字节的指针叫做字符串指针(本身不是字符串),5个字节用来存linux这5个字符才是真正的字符串,最后存'\0'的内存作为字符串结尾标志。
24、字符数组中sizeof计算长度时sizeof(数组名)得到的永远是数组的元素个数(也就是数组个数),strlen计算时只计算'\0'之前的长度。
25、字符数组和字符串指针的本质差异:(1)char a []="linux";定义一个数组a,数组a占6个字节,右值只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有"linux"这个字符串的);这句就相当于是:char a[] ={'l', 'i', 'n', 'u', 'x', '\0'};
(2)字符串char *p ="linux";定义了一个字符指针p,p占4字节,分配在栈上;同时还定义了一个字符串"linux",分配在代码段;然后把代码段中的字符串(一共占6字节)的首地址(也就是'l'的地址)赋值给p。
26、结构体为何要对齐访问:(1)主要是为了配合硬件,也就是说硬件本身有物理的限制,如果对齐排布和访问会提高效率;(2)内存本身是一个物理器件(DDR内存芯片,SoC上的DDR控制器),本身有一定的局限性;如果内存每次访问按照4字节对齐访问,那么效率是最高的。
27、结构体对齐:结构体中每个元素本身都必须对齐存放,对齐后的大小必须是4的倍数,且要求最小内存排布。
28、gcc支持但不推荐的对齐指令:#gragmapack() #gragma pack(n)
以#gragmapack(n)开头,以#gragma pack(n)结尾,定义一个区间,在这个区间内的对齐参数就是n
29、gcc推荐的对齐指令:__attribute__((packed)) __attribute__((aligned(n)))
(1)__attribute__((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
(2)__attribute__((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
30、offsetof宏:
作用:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实是通过编译器来帮我们计算的)
原理:我们虚拟一个type类型的结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。
#defineoffsetof(TYPE, MEMBER) ((int) &((TYPE *)0)->MEMBER)
(TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量。 (实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错)。
((TYPE*)0)- >MEMBER (TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素
&((TYPE*)0)->MEMBER 等效于&(((TYPE *)0)->MEMBER),意义就是得到member元素的地址。但是因为整个结构体变量的首地址是0,
31、container_of宏:
表达:#definecontainer_of(ptr, type, member) ({ \
consttypeof(((type *)0)->member) * __mptr = (ptr); \
(type*)((char *)__mptr - offsetof(type, member)); })
作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。
原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到),减去之后得到的就是整个结构体变量的首地址了,在把这个地址强制类型转换为type*即可。
32、共用体:union myunion 这里面的a和b其实指向同一块内存空间,只是对这块内存空间的2种解析方式的不同,
{ 如果我们使用u1.a,那么久按照int类型来解析这个内存空间;如果我们使用u1.b那么
int a; 就按照char类型来解析这块内存。也就是说共用体的各个成员不是独立的,使用同一个内
int b; 存单元。union中的元素不存在内存对齐问题,因为union实际只有一个内存空间,都是从
同一个地址开始的。 };
33、面试题:判断当前系统的大小端模式
34、由源码到可执行程序的过程:源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->目标文件.o->elf可执行程序
35、预处理主要包括:头文件包含、注释、条件编译、宏定义
36、宏定义实例:(1)max宏: #define max(a,b) ( ( (a)>(b) ) ? (a) : (b) )
#define sec_per_year (365*24*60*60UL) //转为无符号整型
37、带参宏和带参函数的区别:宏定义是在预处理期间处理的,函数是在编译期间处理的,这个区别带来的差异是:宏定义是在调用宏的地方吧宏体原地展开,而函数是在调用函数处跳转到函数中去执行,执行完再跳转回来。因此两者最大差别就是:宏定义没有调用开销,而函数有较大的调用开销。
带参宏和带参函数的一个重要差别就是:宏定义不会检查参数类型,而使用函数时编译器会帮我做参数的静态类型检查。
38、内联函数通过在函数定义前加inline关键字实现,内联函数的本质上是函数,所以有函数的优点(由编译器负责处理,帮我们做参数的静态类型检查),同时也有带参宏的优点(没有调用开销,原地展开),所以可以认为:内联函数就是带了静态类型检查的带参宏,所以内联函数用于函数体很短的函数。
39、static关键字有两种用法,第一种用法是:用来修饰局部变量,形成静态局部变量,本质区别是存储类的不同:非静态全局变量分配在栈上,而静态局部变量分配在数据段/bss段上;第二种用法是:修饰全局变量和函数,形成静态全局变量,本质区别是:链接属性上的不同,普通函数/全局变量默认是外部的,静态的函数/全局变量的链接属性是内部的。
静态局部变量和全局变量在存储类和生命周期方面一样,在作用域和链接属性方面不同,静态局部变量作用域是代码块作用域、链接属性是无;全局变量作用域是文件作用域、链接属性是外连接。
40、register只有一个作用:修饰的变量编译器会尽量将它分配在寄存器中,所以register修饰的变量用在那种变量被反复高频率的使用,可以极大的提升程序运行效率时。
41、C语言中声明全局变量时不能加初始化,如果加了编译器就会把这个声明当作定义(即重复定义)
42、volatile用来修饰一个变量,表示这个变量可以被编译器之外的东西改变,也就是这个改变不是当前代码造成的,编译器在编译当前代码时无法预知。譬如在中断处理程序isr中更改了这个变量的值,譬如多线程中在别的线程中更改了这个变量的值,譬如硬件自动更改了这个变量的值(一般这个变量是一个寄存器的值)。此时编译器在遇到volatile修饰的变量时就不会对该变量的访问进行优化,就不会出现错误。
a=3;b=a;c=b;对于这样的代码不优化的情况是将每个变量都取出来一次共取出3次,优化后只需从内存取一次赋值一次即可,但这中间若有上述3种情况改变了其中变量的值,但编译器并不知道,还会把他们赋值为3,出现错误。
43、c99中支持的关键字restrict,只用来修饰指针,不能修饰普通变量。
44、三种链接属性:外链接:就是说可以在整个程序范围内(也就是跨文件)进行链接,譬如普通的函数和全局变量属于外链接;内链接:就是只在自己c文件内部进行链接,就是不能在当前文件外面的其他文件进行访问链接,static修饰的函数/全局变量属于内链接;无链接:是说这个符号不参与链接,所有的局部变量(auto、static)都是无链接的。
45、C语言链接属性解决重名问题思路:将明显不会在其他c文件中引用(只在当前文件中引用)的函数/全局变量,使用static修饰使其成为内链接属性,这样将来链接时即使2个c文件中有重名的函数/全局变量,只要其中一个/两个为内链接属性就没事。
46、注意区分全局变量的定义和声明:如果定义的同时有初始化则一定会被认为是定义;如果只是定义而没有初始化则可能被编译器认为是定义,也可能被认为是声明。所以多个文件中同时出现相同变量的定义但未初始化,则编译器就会默认把其中一个当成定义,其余的当成声明,所以不会出现错误。
47、存储类决定生命周期(栈上的是临时的,数据段上的是永久的),作用域决定链接属性(代码块作用域决定是无链接,文件作用域是内链接或外链接)。