引言
本篇结合多段面试经历,在参考大量优质博客基础上,言简意赅地总结出如下C/C++高频面试点
目录
- 内存分布
- switch
- do{…}while(0)
- 枚举
- 结构体与共用体
- 全局变量与局部变量
- 数据类型
- 类型转换
- 指针数组与数组指针
- 指针函数与函数指针
- 野指针与空指针
- 内存泄漏与内存溢出
- malloc、calloc与realloc
- sizeof与strlen
1. 内存分布
名称 | 内容 |
---|---|
代码段 | 可执行代码、字符串常量 |
数据段 | 已初始化全局变量、已初始化全局静态变量、局部静态变量、常量数据 |
BSS段 | 未初始化全局变量,未初始化全局静态变量 |
栈段 | 局部变量、函数参数 |
堆段 | 动态内存分配 |
注:栈段亦由操作系统分配和管理,而不需要程序员显示地管理;堆段由程序员自己管理,即显示地申请和释放空间
选项 | 栈 | 堆 |
---|---|---|
存储内容 | 局部变量 | 变量 |
作用域 | 函数作用域、语句块作用域 | 函数作用域、语句块作用域 |
编译期间大小是否确定 | 是 | 否 |
大小 | 1MB | 4GB |
内存分配方式 | 地址由高向低减少 | 地址由低向高增加 |
内容是否可以修改 | 是 | 是 |
选项 | 数据段/BSS段 | 代码段 |
---|---|---|
存储内容 | 全局变量、静态变量 | 常量 |
编译期间大小是否确定 | 是 | 是 |
内容是否可以修改 | 是 | 否 |
2. switch
switch语句允许测试变量与值列表的相等性,每个值称之为案例或者case,程序会检查switch后面的值并且与case后面的值比对,如果相等则执行后面的代码或代码块
- 当switch后面的变量值和case后面的常量值匹配相等后,case后面的代码将会被执行,直到break语句被执行后跳出switch代码块
- break不是必须的,如果没有break,则执行完当前case的代码块后会继续执行后面case代码块的内容,直到执行break才可以退出
- switch有一个默认的情况,我们用default关键词表示,当switch后面的变量和所有case后面的常量都不匹配的情况下,默认执行default后面的语句
注意:
- switch语句中使用的表达式必须具是int或enum类型,否则如float等其他数据类型是无法通过的编译的,因为编译器需要switch后面的语句和case后面的值精确匹配,而计算机无法精确表达一个float数据类型
- switch可以任意个case语句(包括没有), 值和语句之间使用:分隔
- case后面的值必须是int常量值,或者返回结果为int类型的表达式
3. do{…}while(0)
在实际开发过程中,循环更多采用for和while,而do{…}while()主要有以下作用:
#define是在预处理的时候进行直接替换,缺少相应的语法检查机制,如下:
#define LOG {print();send();};
void print()
{
cout<<"print: "<<endl;
}
void send()
{
cout <<"send: "<<endl;
}
int main(){
if (false)
LOG
cout <<"hello world"<<endl;
system("pause");
return 0;
if经过预处理替换会变成:
if (false)
{
print();
send();
};
else
{
cout <<"hello"<<endl;
}
因为if语句后面多加了个;而编译不通过
用do{…}while(0);可以包裹住要操作的#define,无论外面如何操作,都不会影响#define的操作:
#define LOG do{print();send();}while (0);
int main(){
if (false)
LOG
else
{
cout <<"hello"<<endl;
}
cout <<"hello world"<<endl;
system("pause");
return 0;
}
if则会变成:
if (false)
do{
print();
send();
}while (0);
else
{
cout <<"hello"<<endl;
}
cout <<"hello world"<<endl;
编译通过
4. 枚举
枚举类型是C语言和C++中的一种派生数据类型,它是由用户定义的若干枚举常量的集合,所谓"枚举"是指将变量的值一一列举出来,变量的值只能在列举出来的值的范围内
注意:
- 第一个名称的值为 0
- 默认情况下,每个名称都会比它前面一个名称大 1
enum color1
{
red,
green,
blue
} c1;
c1 = blue; //red的值为 0,green的值为 1,blue 的值为 2,c1的值为2
enum color2
{
red,
green=5,
blue
};c2
c2 = blue; //red的值为 0,green的值为 5,blue 的值为 6,c2的值为6
5. 结构体与共用体
定义:
- 结构体是C/ C++ 中另一种用户自定义的可用的数据类型,它允许存储不同类型的数据项
- 共用体是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型,可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值,共用体提供了一种使用相同的内存位置的有效方式
区别:
- 结构体占用的内存是所有的成员各自占用的内存空间之和
- 共用体占用的内存则不同,等于占用内存空间最大的那个成员
注:共用体是共用内存空间,所以每个成员都是读写同一个内存空间,那么内存空间里面的内容不停的被覆盖,而同一时刻,都只能操作一个成员变量
union Data{
int i;
float f;
char str[20];
};
int main( ){
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
结果如下:
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
我们可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因
6. 全局变量与局部变量
定义:
- 在函数或一个代码块内部声明的变量,称为局部变量。它们只能被函数内部或者代码块内部的语句使用
- 在所有函数外部定义的变量(通常是在程序的头部),称为全局变量。全局变量的值在程序的整个生命周期内都是有效的
初始化:
- 定义局部变量时,系统不会对其初始化,必须自行对其初始化
- 定义全局变量时,系统会自动初始化为下列值
int — 0 char — ‘\0’ float — 0 double — 0 pointer — NULL
注意:局部变量和全局变量的名称可以相同,但是在函数内,局部变量的值会覆盖全局变量的值
7. 数据类型
32位
类型 | 字节大小 |
---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4 |
float | 4 |
long long | 8 |
double | 8 |
指针类型:均为4字节
64位
类型 | 字节大小 |
---|---|
char | 1 |
short | 2 |
int | 4 |
long | 4 |
float | 4 |
long long | 8 |
double | 8 |
指针类型:均为8字节
8. 类型转换
// 普通转换
int sum = 7;
double mean = sum / 5; //mean为 1.0
// 强制转换
int sum = 17;
double mean = (double) sum/5;//mean为1.4
9. 指针数组与数组指针
定义:
指针数组:指针的数组,是一个数组,只不过数组的元素存储的是指针变量
数组指针:数组的指针,是一个指针变量,指向了一个一维数组
注意:在定义的时候,符号优先级:()>[]>*
//指针数组
char *arr[4] = {"hello", "world", "shannxi", "xian"};
//arr就是指针数组,它有四个元素,每个元素是一个char *类型的指针,这些指针存放着其对应字符串的首地址。
//数组指针
char (*pa)[4];
//pa就是指向数组的指针
实际代码开发使用示例:
指针数组
指针数组的排序非常有趣,因为这个数组中存放的是指针,通过比较指针指向的空间的大小,排序这些空间的地址。函数实现如下
void sort(char **pa, int n)//冒泡排序
{
int i, j;
char *tmp = NULL;
for(i = 0; i < n-1; i++){
for(j = 0; j < n-1-i; j++){
if((strcmp(*pa+j), *(pa+j+1)) > 0){
tmp = *(pa + j);
*(pa + j) = *(pa + j + 1);
*(pa + j + 1) = tmp;
}
}
}
}
在函数中定义指针数组,并且打印结果如下:
char *pa[4] = {"abc", "xyz", "opq", "xyz"};
[root@menwen-linux test]# ./test
abc
ijk
opq
xyz
数组指针
数组指针既然是一个指针,那么就是用来接收地址,在传参时就接收数组的地址,所以数组指针对应的是二维数组
void fun(int (*P)[4]);//子函数中的形参,指针数组
a[3][4] = {0};//主函数中定义的二维数组
fun(a);//主函数调用子函数的实参,是二维数组的首元素首地址
10. 指针函数与函数指针
定义:
指针函数:指针的函数,其本质是一个函数,函数的返回值是一个指针
函数指针:函数的指针,其本质是一个指针变量,该指针指向这个函数
// 指针函数
int *fun(int x,int y);//其返回值是一个 int 类型的指针,是一个地址
//函数指针
int (*fun)(int x,int y);//该指针指向这个函数
//函数指针是需要把一个函数的地址赋值给它,有两种写法:
fun = &Function;
fun = Function;
注意:回调函数是函数指针使用示例
11. 野指针与空指针
定义:
- 野指针的值并不为null,野指针会指向一段实际的内存,只是它指向哪里我们并不知情,或者是它所指向的内存空间已经被释放
- 空指针是指一个指针的值为null
造成野指针原因:
- 指针变量的值未被初始化
void func(){
int *ptr; // 野指针
}
- 指针所指向的地址空间已经被free或delete
void func(){
char *p = (char *)malloc(sizeof(char)*100);
free(p);//p所指向的内存被释放,但是p指针还会继续指向这段堆上已经被释放的内存
}
- 指针操作超越了作用域
void func(){
int *ptr = nullptr;
{
int a = 10;
ptr = &a;
} // a的作用域到此结束
int b = *ptr; // ptr指向a,a已经被回收,ptr野指针
}
解决办法
- 初始化置NULL
void func(){
int *ptr = NULL; // 野指针
}
- 申请内存后判空(malloc申请内存后需要判空,而在现行C++标准中,如C++11,使用new申请内存后不用判空,因为发生错误将抛出异常)
void func(){
char *p = (char *)malloc(sizeof(char)*100);
assert(p != NULL); //判空,防错设计
free(p);//p所指向的内存被释放,但是p指针还会继续指向这段堆上已经被释放的内存
}
3.指针释放后置NULL
void func(){
char *p = (char *)malloc(sizeof(char)*100);
assert(p != NULL); //判空,防错设计
free(p);//p所指向的内存被释放,但是p指针还会继续指向这段堆上已经被释放的内存
p = NULL; //释放后置空
}
12. 内存泄漏与内存溢出
定义:
内存泄漏:指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
内存溢出:指程序申请内存时,没有足够的内存供申请者使用
堆栈溢出
原因 | 解决办法 |
---|---|
开了数据非常大的局部数据结构,比如数组 | 大的数组尽量不要定义在函数内部 |
过多的递归调用,使用了大量的空间 | 递归注意深度 |
有死循环,不断的往堆栈中写入数据 | 不要造成函数死循环 |
13. malloc、calloc与realloc
相同点:三者都是分配内存
不同点:
- malloc函数:原型void *malloc(unsigned int num_bytes),num_byte为要申请的空间大小,需要我们手动的去计算
- calloc函数:原型void *calloc(size_t n, size_t size),其比malloc函数多一个参数,并不需要人为的计算空间的大小,例如int *p = (int *)calloc(20, sizeof(int))
- realloc函数:原型void realloc(void *ptr, size_t new_Size),用于对动态内存进行扩容,ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小
14. sizeof与strlen
sizeof:运算符(编译时计算),其能够返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系
注:由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小
strlen:函数(运算时计算),返回字符串的长度,直到遇到结束符’\0’,返回的长度大小不包括’\0’
char arr[10] = "Hello";
int len_one = strlen(arr);
int len_two = sizeof(arr);
cout << len_one << " and " << len_two << endl; //5 and 10
总结:
sizeof返回数组时,只管编译器为其分配的数组空间大小,不关心里面存了多少数据。
strlen只关心存储的数据内容,不关心编译器为其分配的数组空间大小