C/C++编程基础部分常见问题梳理

C/C++基础开发知识复习总结。

基础部分


1、变量声明和定义有什么不同?

  • 声明仅仅是将变量的名字和类型告诉编译器,并不分配存储空间。例如:extern int a;
  • 定义则会为其分配存储空间,也可能会为变量赋一个初始值。例如:int a; int pi = 3.14; // 声明并定义;
  • 变量能且只能被定义一次,但可以被多次声明;
  • 另外:extern double pi = 3.14,此时对extern变量赋予初值,就变成定义了。

2、零值比较?

  • bool类型:if(flag)
  • int类型:if(flag == 0)
  • 指针类型:if(flag == null)
  • float类型:if((flag >= -0.000001) && (flag <= 0.000001))

3、strlen和sizeof区别?

  • sizeof是运算符,并不是函数,它的结果是在编译而非运行中获得的,strlen是字符处理的库函数;
  • sizeof参数可以是任何数据的类型或者结构(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。

4、相同类的同一对象可以相互赋值吗?

  • 可以,但是要对含有指针成员时需注意;
  • 深拷贝和浅拷贝的区别:浅拷贝只是增加了一个指向相同内存的指针,而深拷贝是重新申请了一块内存,并把原来内存的值拷贝过来,使的拷贝的指针指向新的内存的指针。

5、结构体内存对齐问题?

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同;
  • 未特殊说明时,按结构体中size最大的成员对齐(若有double成员),按8字节对齐。

6、struct和class的区别?

  • struct默认访问修饰符是public,class默认访问修饰符是private。

7、malloc和new的区别?

  • malloc和free是标准库函数,支持覆盖;new和delete是运算符,并且支持重载;
  • malloc仅仅分配内存空间,free仅仅回收内存空间,不具备调用构造函数和析构函数的功能,用malloc函数分配存储类的对象存在风险;new的delete除了分配回收功能外,还能调用构造函数和析构函数。
  • malloc返回的是void类型的指针,必须进行类型转换,而new和delete返回的是具体类型指针。

8、指针和引用的区别?

  • 引用只是别名,并不占用具体存储空间,只有声明而没有定义;指针是具体变量,需要占用存储空间;
  • 引用一旦初始化之后就不可以再改变(变量可以被引用多次,但引用只能作为一个变量引用);指针变量可以重新指向;
  • 不存在引用的引用,引用必须是实体;而存在指针的指针。

9、宏定义和函数的区别?

  • 宏定义是在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用运行时需要跳转到具体函数;
  • 宏函数属于在结构体中插入代码,没有返回值;函数调用具有返回值;
  • 宏函数参数没有类型,不进行类型检查;函数参数有具体类型,需要检查类型;
  • 宏函数不要在后面加封号。

10、define和const的区别?

  • 宏替换发生在编译预处理时,属于文本插入;const作用发生于编译过程中;
  • 宏不检查类型;const会做类型安全检查;
  • 宏定义的数据没有分配空间,只是简单的替换;const定义的变量不能改变,但是要分配空间。

11、宏定义和typedef区别?

  • 宏定义主要用来定义常量和书写复杂且重复的内容;typedef主要用于定义类型别名;
  • 宏替换发生在编译预处理阶段,属于文本插入替换;typedef是编译的一部分;
  • 宏不检查类型;typedef会检查数据类型;
  • 宏不是语句,后面不需要加分好;typedef是语句,要加分号标识结束。

12、宏定义和内联函数(inline)的区别?

  • 在使用时,宏只是简单字符串替换(编译预处理);而内联函数可以进行参数类型检查(编译时),且具有返回值;
  • 内联函数本身就是函数,强调函数特性,且具有重载等功能;
  • 内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了。

13、条件编译#ifdef,#else,#endif作用?

  • 可以通过#define,并通过#ifdef来判断,将某些具体模块包括进要编译的内容;
  • 用于子程序前加#define DEBUG用于程序调试;
  • 应对硬件的设置(机器类型等);
  • 条件编译功能if也可以实现,但条件编译可以减少被编译语句,从而减少目标程序大小;

14、区分一下几种变量?

const int a
int const a
const int *a
int *const a

  • int const a 和 const int a均表示常量类型a
  • const int *a,常量指针,其中a是指向const int的变量指针,a的值可以变化
  • int *const a,指针常量,表示a是指向int变量的指针,但a的值不能发生变化。

15、volatile有什么作用?

  • volatile定义变量的值是易变的,每次用到这个变量的值都要重新读取这个变量的值,而不是读寄存器内的备份;
  • 多线程中几个被任务共享的变量需要定义成volatile类型。‘

16、什么是常引用?

  • 常引用可以理解为常量指针,形式为const typename& refname=varname;
  • 常引用下,原变量值不会被别名所修改;
  • 原变量值可以通过原名修改;
  • 常引用通常用作只读变量别名或是形参传递。

17、区分以下指针类型?

int *p[10];
int (*p)[10];
int *p(int)
int (*p)(int)

  • int *p[10]表示指针数组,是一个数组变量,大小为10,数组内的元素指向int类型的指针变量;
  • int (*p)[10]表示数组指针,是指针类型,指向的是一个int类型的数组,这个数组的大小是10;
  • int *p(int)是函数声明,函数名是p,参数类型是int,返回值int*
  • int (*p)(int)是函数指针,是指针类型,指向的具有int参数,返回值是int的函数

18、a和&a有什么区别?

假设数组int a[10]
int (*p)[10] = &a

  • a是数组名,是数组首元素的地址,+1表示地址值加上一个int类型的大小,*(a+1)=a[1];
  • &a是数组的地址,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址;
  • 若(int *)p ,此时输出 *p时,其值为a[0]的值,因为被转为int *类型,解引用时按照int类型大小来读取。

19、数组名和指针(这里为指向数组首元素的指针)区别?

  • 二者均可以通过增减偏移量来访问数组中的元素;
  • 数组名不是真正意义上的指针,可以理解为指针常量,所以数组名没有自增、自减等操作;
  • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

20、野指针是什么?

  • 野指针也叫空悬指针,不是指向null的指针,指向垃圾内存的指针;
  • 产生的原因及解决办法:
    • 指针变量未及时初始化=>定义指针变量要及时初始化,要么置空
    • 指针free或delete之后没有及时置空=>释放操作后立即置空

21、堆和栈的区别?

  • 申请方式不同:
    • 栈由系统分配;
    • 堆由程序员手动分配。
  • 增长方向不同:
    • 栈是从高地址向低地址增长;
    • 堆是从地址向高地址增长;
  • 大小限制不同:
    • 栈顶和栈底是之前预设好的,大小固定,可以通过ulimit -a查看,由ulimit -s修改;
    • 堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
  • 申请效率不同:
    • 栈由系统分配,速度快,不会有碎片;
    • 堆由程序员分配,速度慢,会有碎片。

22、delete和delete[]的区别?

  • delete只会调用一次析构函数
  • delete[]会调用数组中每个元素的析构函数

编译及调试

1、编译

  • 预处理

    • 展开所有宏定义。
    • 处理条件编译语句,通过是否具有某个宏过滤掉哪些代码。
    • 处理#include指令,将被包含的文件插入到该指令所在位置。
    • 过滤掉注释语句。
    • 添加行号和文件名。
    • 保留所有#pargma编译器指令
  • 编译

    • 词法分析
    • 语法分析
    • 语义分析
    • 中间代码生成
    • 目标代码生成与优化

2、链接

各个源代码模块独立的被编译,然后将他们组装起来成为一个整体,组装的过程就是链接。被链接的各个部分本本身就是二进制文件,所以在被链接时需要将所有目标文件的代码段拼接在一起,然后将所有对符号地址的引用加以修正。

  • 静态链接
    静态链接最简单的情况就是在编译时和静态库链接在一起成为完整的可执行程序。这里所说的静态库就是对多个目标文件(.o)文件的打包,通常静态链接的包名为lib**.a,静态链接所有被用到的目标文件都会复制到最终生成的可执行目标文件中。这种方式的好处是在运行时,可执行目标文件已经完全装载完毕,只要按指令序执行即可,速度比较快,但缺点也有很多。
    打包命令:
    1
    2
    3
    gcc -c test1.c
    gcc -c test2.c
    ar cr libtest.a test1.o test2.o

首先编译得到test1.o和test2.o两个目标文件,之后通过ar命令将这两个文件打包为.a文件,文件名格式为lib + 静态库名 + .a后缀。在生成可执行文件需要使用到它的时候只需要在编译时加上即可。需要注意的是,使用静态库时加在最后的名字不是libtest.a,而是l + 静态库名。

1
gcc main.c -o main -ltest

  • 动态链接
    静态链接发生于编译阶段,加载至内存前已经完整,但缺点是如果多个程序都需要使用某个静态库,则该静态库会在每个程序中都拷贝一份,非常浪费内存资源,所以出现了动态链接的方式来解决这个问题。

动态链接在形式上倒是和静态链接非常相似,首先也是需要打包,打包成动态库,不过文件名格式为lib + 动态库名 + .so后缀。不过动态库的打包不需要使用ar命令,gcc就可以完成,但要注意在编译时要加上-fPIC选项,打包时加上-shared选项。

1
2
3
gcc -fPIC -c test1.c
gcc -fPIC -c test2.c
gcc -shared test1.o test2.o -o libtest.so

使用动态链接的用法和静态链接相同。

1
gcc -o main mian.c -ltest

如果仅仅像上面的步骤是没有办法正常使用库的,我们可以通过加-Lpath指定搜索库文件的目录(-L.表示当前目录),默认情况下会到环境变量LD_LIBRARY_PATH指定的目录下搜索库文件,默认情况是/usr/lib,我们可以将库文件拷贝到那个目录下再链接。

静态库和动态库的优缺点:

  • 动态库运行时会先检查内存中是否已经有该库的拷贝,若有则共享拷贝,否则重新加载动态库(C语言的标准库就是动态库)。静态库则是每次在编译阶段都将静态库文件打包进去,当某个库被多次引用到时,内存中会有多份副本,浪费资源。
  • 动态库另一个有点就是更新很容易,当库发生变化时,如果接口没变只需要用新的动态库替换掉就可以了。但是如果是静态库的话就需要重新被编译。
  • 不过静态库也有优点,主要就是静态库一次性完成了所有内容的绑定,运行时就不必再去考虑链接的问题了,执行效率会稍微高一些。

参考资料:
编程语言(C/C++)