1. printf() 为什么需要输出控制符?
输出控制符:%d、%c 等
答:计算机中任何信息都是以 0 1 0 1 组合形式存在的,对于同样的 0 1 组合信息,计算机不知道该组合数据是一个整数还是一个其他类型的数,所以必须有个控制符来告诉计算机。
2. 一维数组名是变量吗?
如:
1 | int a[5]; |
答:一维数组名 a 是常量,它等于这个数组第一个元素的地址。
所以 a = b;
是错误的,因为常量不能被赋值。
其中a[0]、a[1] 元素等是变量。
3. 一维数组如何赋值?
- 定义的同时进行赋值,只有这样才能整体赋值
int a[] = {1, 3, 12, 5}; //4个元素
int b[8] = {1, 3, 12, 5}; //8个元素,后5个为0
- 先定义后赋值,必须逐个赋值了
1 | int a[5]; |
注意:这个性质与结构体相似,但是结构体可以通过强制类型转换实现后期整体赋值
一般情况:
1 | typedef struct student { |
先定义后赋值特殊处理办法:
1 | Student s2; |
4. 使用指针有什么好处?
指针即地址,地址即内存单元的编号,一字节为一单元。
对于 32 位机器,即 32 根地址总线,能够访问 2^32 = 4G 个地址单元,所以老电脑最大只能支持 4G 内存。
指针有什么好处:
- 数据结构中,需要利用指针,比如链表、树、图;所以学习指针是为了数据结构打基础;
- 指针可以快速地传递数据,并且节省内存。因为不用复制整个数据进行传输,而仅仅传输该数据的指针(地址)即可;
- 被调函数可以利用指针返回多个值,如果没有指针只能返回一个值给主调函数;
- 相较于字符数组,指针处理字符串更方便;
- 指针可以直接访问硬件。
5. 变量与 0 比较
- 整数 0
1 | if ( p == 0 ) |
- bool 类型
1 | if ( p ) |
bool 类型的 True 和 False 在 C 语言中是通过 #define True 1
、#define False 0
的形式定义的。如果写成 if ( p == True )
就等价于 if ( p == 1 )
,这就出问题了,因为实际上 bool 类型的 True 的实际含义是「非0的所有值,而非仅仅指1」。
- 指针类型
1 | if ( p == NULL ) |
虽然 NULL 值为 0,但是含义不同,NULL 表示的是内存单元的编号 0,即 0x0000000000000000 地址。
计算机规定了以 0 为编号的内存单元不可读、不可写。
6. 传统数组(静态数组)的缺陷
- 内存空间的分配和释放是系统控制的。函数运行期间,系统为该函数内部的数组分配空间,待该函数运行完毕时,系统释放该数组内存空间。
- 进而导致 A 函数一旦运行结束,那么其他 B 函数就无法使用 A 中的数组变量了,即静态数组无法跨函数使用。
- c11 标准前,数组定义时必须指定长度,而且在函数运行过程中无法修改数组长度。
为了消除以上的缺陷,引入了动态数组,即动态内存分配的数组。
7. 动态内存分配
- 什么叫动态内存分配
malloc() 函数原型:(void *)malloc( int len )
,表示向系统申请 len 个字节的内存空间,如果申请成功则返回第一个字节的地址,如果失败,则返回 NULL。
- 为什么要强制类型转换?
如:int * p = (int *)malloc(50);
,向系统申请50个字节的内存空间,malloc 函数返回第一个字节的地址,但是这个地址是无意义的,需要转换为相应数据类型的地址才可以使用。
换言之就是,malloc 返回第一个字节的地址,经过(int *)
强制类型转换后,返回的就是4个字节的地址,那么 p 指针变量,指向的就是这4个字节,而非一个字节。那么p + 1
就指向了第二个4字节。
如:double * p = (double *)malloc(50)
,将 malloc 返回的第一个字节地址转换为 double * 型的地址,即将第一个字节的地址转换为8个字节地址,那么p + 1
就指向了第二个8字节。
8. 内存释放
对于int * p = (int *)malloc(50);
语句,系统分配了两块内存,一块是动态分配的50个字节的内存,一块是静态分配的 p 变量本身的内存(64位系统占用8字节)。
动态内存需要程序员手动释放:free(p)
。静态内存只能由系统来释放,即 p 本身的内存只能在 p 变量所在的函数终止时由系统自动释放。
注意:函数运行中,free(p)
释放了 p 指向的那个地址的内存,然后系统会将该地址交给其他程序使用。但是 p 变量本身的内存仍然存在,那么通过 p 依然可以找到被释放的那个内存,这就存在安全隐患了。
所以,不能对一个地址使用两次free(p)
,否则会破坏其他程序。
最好这么使用:
1 | free(p); |
C++演示:
1 |
|
9. 内存的五个部分
静态存储区:
存储:全局变量、static 变量;
生命周期:由编译器在编译时分配内存,整个程序结束后销毁;
特点:编译时未赋值的变量系统会自动赋初值0(数值型变量)或空字符(字符变量);
栈:
变量:局部变量;
生命周期:函数结束后销毁;
特点:效率高,空间有限;
堆:
变量:由 malloc 系列函数(C语言)或 new 操作符(C++)分内存的变量;
生命周期:程序员手动释放,由 free(C语言) 或 delete(C++) 决定;
特点:使用灵活,空间比较大,但容易出错;
常量存储区:
如:char * s = “Hello World”;
特点:只读,无法修改;
程序代码区:
程序运行时的函数体的二进制代码;
10. static 修饰符
修饰全局变量 -> 静态全局变量:
内存中的位置:静态存储区,不变;
初始化:自动初始化为 0;
作用域:只限于声明该变量的文件,而普通全局变量可以被所有源程序共享;
修饰局部变量 -> 静态局部变量:
内存中的位置:由「栈」变为了「静态存储区」;
初始化:未经初始化情况下,由分配垃圾值 -> 自动初始化为 0;
作用域:仍然为函数内部;
生命周期:函数运行周期 -> 整个程序运行周期;
修饰函数 -> 静态函数:
这里的 static 的作用不是改变存储位置,而是同修饰全局变量一样,改变了函数的作用域,由所有源文件缩小为仅限于本文件。
静态函数又称为内部函数,其优点:不同人编写不同函数时,不用担心其他人编写的文件里有同名函数。
总结:static 用来表示不能被其他文件访问的全局变量和函数,将在栈中分配的局部变量变为静态存储区的变量。
11. const 修饰指针的几个例子
const 修饰的变量或者函数,表示只读,不可更改。
const int * p;
== int const * p;
:*p 不可变(即 p 指向的对象不可变),但 p 可以变;
int * const p
:p 不可变,p 指向的对象可变;
总结:const 离谁近,就修饰谁。
12. extern 的用法
使用场景:
在 a.c 文件中定义了函数 int fun(int nu)
和全局变量 int v
;
要在 b.c 文件中使用 fun() 函数和变量 v;
那么其中一个方法就是使用 extern 来实现:在 b.c 文件中使用 extern int fun(int )
、extern int v
进行外部声明,这样就可以在源文件 b.c 中使用源文件 a.c 里的函数和全局变量了。
举例:
1 | // a.c |
输出结果:
**去掉 b.c 中的extern int afun(int)
**,输出结果为:
一般对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件
那么,两者有什么区别呢?请往下看:
- 使用 extern 和包含头文件来引用函数有什么区别?
extern 相对来说的优点:
简洁。直截了当,想引用哪个函数就用 extern 声明哪个函数,符合 KISS 原则;
加速程序的编译过程,节省时间。在大型 C 程序编译过程中,这种差异相当明显。
此外, extern 修饰符可用于指示 C 或者 C++ 函数的调用规范。比如在 C++ 程序中用extern "C"
声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候使用 C 函数规范。主要原因是 C++ 和 C 程序编译完成后在目标代码中命名规则不同。
13. volatile 的用法
volatile 的中文意思是「易变的」,这里主要是对编译器说的,告诉编译器该变量易变,不要去优化包含该变量的代码,从而可以提供对特殊地址的稳定访问。
举例1:
1 | int i = 10; |
针对以上代码,编译器在编译时会进行优化,因为在 1、2 两条语句中,变量 i 没有被用作左值。
这时候编译器会认为 i 的值没有发生变化,所以执行语句 1 时从内存中取出 i 的值赋给变量 j 后,这个值并没有丢掉,而是在语句 2 时继续用这个值给变量 k 赋值。
编译器不会生成再次从内存中取 i 的值的汇编代码,这样就提高了效率。
举例2:
1 | volatile int i = 10; |
volatile 告诉编译器 i 值是可能随时变化的,每次使用它的时候必须从内存中重新读取 i 的值。
主要用在:i 是一个寄存器变量或者表示一个端口或者是多个线程的共享数据。因为这些地方 i 的值随时都可能变化。
14. 预处理命令
未完待续。。。