编译和调试
C/C++程序编译过程
- C/C++程序编译过程就是把C/C++代码百年成可执行文件的过程, 该过程分为4步
- 预处理阶段
- 进行宏展开和宏替换
- 处理条件编译指令, 如#ifdef, #endif等
- 去掉注释
- 添加行号和文件名标识
- 保留#pargma编译器指令(#Pragma命令将设定编译器的状态或者是指示编译器完成一些特定的动作)
- 编译程序所要作的工作就是通过词法分析, 语法和语义分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
- 代码优化
- 重点关注函数压栈方式的编译处理__cdecl是C DECLaration的缩写, 表示C语言默认的函数调用方法: 所有参数从右到左依次入栈. 这些参数由调用者清除,称为手动清栈. 被调用函数不需要求调用者传递多少参数, 调用者传递过多或者过少的参数. 甚至完全不同的参数都不会产生编译阶段的错误。_stdcall 是StandardCall的缩写, 是C++的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话, 最后一个入栈的是this指针。这些堆栈中的参数由被调用的函数在返回后清除, 使用的指令是 retnX,X表示参数占用的字节数. CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈. 函数在编译的时候就必须确定参数个数. 并且调用者必须严格地控制参数的生成,不能多, 不能少, 否则返回后会出错。
- 汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程
- 对于被翻译系统处理的每一个C语言源程序, 都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
- 目标文件由段组成. 通常一个目标文件中至少有两个段: 代码段和数据段
- 将所有的目标文件代码拼接且重定位符号地址, 生成可执行文件
- 两种链接方式: 动态链接和静态链接
动态链接和静态链接区别
- 在编译时静态库和程序链接
- 一般命名为:libxxx.a
- 运行时,可执行目标文件已经装载完毕,速度快
- 但是多个程序需要静态库时每个都会复制一份,造成内存浪费
- 更新后需要重新编译
- 在运行时链接
- 一般命名: libxxx.so
- 运行时加载链接, 速度相对慢
- 运行时多个程序共享同一份动态库, 不会造成内存浪费
- 易更新, 无需重新编译
内存管理
new/delete和malloc/free区别
- new是C++关键字,需要编译器支持. 而malloc是C语言库函数
- new失败时, 会抛出bad_alloc异常. malloc会返回NULL
- new执行时, 先分配内存, 再调用类的构造函数初始化, malloc只会分配内存
- new无需指定分配内存大小, 编译器会根据类型信息自行计算, malloc需要在参数里指出分配内存的大小
- new成功会直接返回所分配的对象的指针, 而malloc只会返回void指针, 需要转化才能得到想要的对象的指针
- C++允许重载new/delete操作符, 而malloc不允许重载, new从自由存储区上为对象动态分配内存空间, malloc从堆上分配内存
C++中有几种类型的new
- plain new: 普通new, 特点在new和malloc区别里说了
- nothrow new: 在空间分配失败时不会抛出异常, 而是返回NULL. 使用:
char *p=new(nothrow) char[10e11]
- new operator: 只做两件事:(1)调用operator new (2)调用类的构造函数
- operator new: 可以重载, 实际以标准C malloc()完成
- placement new: 在一块已经分配成功的内存上重新构造对象或对象数组. 不会出现内存分配失败, 因为他只调用构造函数而不会分配内存. 用placement new构造和对象数组, 要显式调用它们的析构函数来销毁, 而不要用delete[], 因为构造起来的对象或数组大小不一定等于原来内存的大小, 用delete会造成内存泄漏或运行时出现错误. 使用:
#include <iostream>
#include <string>
using namespace std;
class ADT{
int i;
int j;
public:
ADT(){
i = 10;
j = 100;
cout << "ADT construct i=" << i << "j="<<j <<endl;
}
~ADT(){
cout << "ADT destruct" << endl;
}
};
int main()
{
char *p = new(nothrow) char[sizeof ADT + 1];
if (p == NULL) {
cout << "alloc failed" << endl;
}
ADT *q = new(p) ADT; //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
//delete q;//错误!不能在此处调用delete q;
q->ADT::~ADT();//显示调用析构函数
delete[] p;
return 0;
}
malloc原理
- Malloc一般是从heap分配. heap会事先分配n个页, 放在一起, 并在里面做些标记表明哪些块是使用中的哪些是空闲的. malloc只是从这里面找到一个空闲的块。
- 当开辟的空间小于 128K 时, 调用 brk()函数
- 当开辟的空间大于 128K 时,调用mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为"文件映射区域"的地方)找一块空间来开辟
C++内存分区
- 自高地址到低地址依次是栈, 对, 全局数据区, 常量区和代码区
- 栈是程序的局部变量存储区域, 分配效率高, 但是内存容量有限, 同时离开变量作用域后会存储单元会自动释放
- 堆是程序由malloc分配的内存块, 需要手动释放内存空间
- 自由存储区: new操作符从自由存储区(free store)上为对象动态分配内存空间,自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。自由存储区通常是堆
- 全局/静态存储区: 全局变量和静态变量存储的区域, 未初始化的静态变量会自动初始化为0
- 代码区: 存放函数体的二进制代码
面向对象
面向对象编程的三大特征
纯虚函数和虚函数
- 纯虚函数在定义类不能被实现, 只有继承类才能被实现. 可以认为是一种接口函数含有纯虚函数的类被称为抽象类
- 虚函数是为了重载和多态需要, 在基类中可以实现, 子类重写后会覆盖基类的虚函数
构造函数中能调用其他成员函数吗
- 可以, 但是不能调用类的静态成员函数, 虚函数以及以来类构造完成的函数
构造函数和析构函数中能用虚函数吗
- 都不能
- 在构造函数中: 父类对象会在子类之前进行构造, 此时子类部分数据成员还没初始化, 调用子类的虚函数时不安全的
- 析构函数: 构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。
构造函数和析构函数能成为虚函数吗
- (1)构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的. 而在构造一个对象时, 由于对象还未构造成功. 编译器无法知道对象的实际类型, 是该类本身, 还是该类的一个派生类. (2)虚函数的执行依赖于虚函数表. 而虚函数表在构造函数中进行初始化工作,即初始化vptr, 让他指向正确的虚函数表. 而在构造对象期间, 虚函数表还没有被初 始化,将无法进行。
- 有虚函数的类的析构函数必须为虚函数. 因为只有序析构函数才能自动调用基类的析构函数.
重载, 覆盖和隐藏区别
- 函数重载: 同一作用域内内, 一组具有不同参数列表的同名函数(静态绑定)
- C++编译器对函数名的处理: 作用域+返回类型+函数名+参数列表
- 参数列表必须不同(个数, 类型或参数排列顺序)
- 仅仅返回值不同不足以称为函数重载
- C语言不支持函数重载, 因为C语言在编译过程会保留原始的函数名
- 覆盖: 重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且函数参数列表必须相同(动态绑定)
- 隐藏: 指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数.
- 两个函数参数相同, 但是基类函数不是虚函数
- 两个函数参数不同, 无论基类函数是不是虚函数, 都会被隐藏
C++多态如何实现
- 多态性: 在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数
- 虚函数表: 在有虚函数的类中,类的最开始部分是一个虚函数表的指针vptr,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
- 虚函数表结构(通过命令g++ -fdump-class-hierarchy xxx.cpp 查看):
- 非空虚基类
class A
{
public:
int a;
virtual void v();
};
sizeof(A ) = 16 ,align=8
- 单继承
class A {
public:
int a;
virtual void v();
};
class B : public A {
public:
int b;
};
sizeof(B) = 16, align = 8
- 简单多继承1
class A {
public:
int a;
virtual void v();
};
class B {
public:
int b;
virtual void w();
};
class C : public A, public B {
public:
int c;
};
sizeof(C) = 32 ,align = 8
- 简单多继承2
class A {
public:
int a;
virtual void v();
};
class B {
public:
int b;
virtual void w();
};
class C : public A, public B {
public:
int c;
void w();
};
sizeof(C) = 32 ,align = 8
- 多重继承
class A {
public:
int a;
virtual void v();
};
class B : public A {
public:
int b;
virtual void w();
};
class C : public A {
public:
int c;
virtual void x();
};
class D : public B, public C {
public:
int d;
virtual void y();
};
sizeof(D) = 40 align = 8
- 虚继承
class A {
public:
int a;
virtual void v();
};
class B : public virtual A {
public:
int b;
virtual void w();
};
class C : public virtual A {
public:
int c;
virtual void x();
};
class D : public B, public C {
public:
int d;
virtual void y();
};
sizeof(D) = 48,align = 8
C++有几种构造函数
- 拷贝构造函数
- 默认构造函数
- 移动构造函数
- 委托构造函数
- 继承构造函数
标准模板库
list和vector区别
- vector: 连续存储的容器,动态数组,在堆上分配空间
- 底层实现:数组
- 两倍容量增长
- 查询时间复杂度为O(1), 插入删除平均时间复杂度为O(n)
- list: 在堆上分配空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间
- 插入删除时间复杂度O(1), 查询时间复杂度O(n)
- 底层实现: 双向链表
迭代器失效的情况
- set和map的操作不会使迭代器失效,因为删除只会导致被删除的元素的迭代器失效,因为被删除的节点不会影响其他节点
- vector的删除会使后面的迭代器都失效
- list的操作不会导致任何迭代器失效
vector扩容原理
- vector是一个能够存放任意类型的动态数组
- vector每次容量满了后会扩容, 一般为当前容量的两倍. 旧内存空间的内容按照原来顺序放到新的内存中
- 最后将旧内存的内容释放掉, 但是存储空间没有释放
set和map区别
- map和set都是C++的关联容器, 其底层实现都是红黑树(RB-Tree). 几乎所有的 map 和set的操作行为, 都只是转调 RB-tree 的操作行为。
- map中的元素是key-value对, 而set只有value
- set迭代器是const, 不允许修改元素的值. map允许修改value的值, 但是不允许修改key的值. 否则会使迭代器失效
- map支持下标操作
set和unordered_set区别
- set是有序的, 底层数据结构是红黑树
- unordered_set是无序的,底层数据结构是哈希表
迭代器和指针区别
- 迭代器不是指针, 各类模板, 表现得像指针. 他只是模拟了指针的一些功能, 通过重载了指针的一些操作符,->、*、++、--等.
- 迭代器本质是封装了原生指针, 是指针概念的一种提升, 提供了比指针更高级的行为
- 迭代器类的访问方式就是把不同集合类的访问逻辑抽象出来, 使得不用暴露集合内部的结构而达到循环遍历集合的效果
基础
static作用
- static变量默认初始化为0
- static全局变量: 表示该变量作用域只在该变量所在的文件中. 在程序运行期间一直存在
- static局部变量: 作用域仍为局部作用域, 但是离开局部作用后变量并没有销毁, 而是依然驻留在内存中, 下次该局部变量在进入作用域时, 值不变, static局部变量只会被初始化一次.
- static函数: 函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。不要在头文件声明static函数
- static成员变量: 静态成员可以实现多个对象之间的数据共享. 即无论对象有几个, 一个类中的static成员变量只有一个
- static成员函数: 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员. 没有this指针, 所以只能用类的静态成员变量和静态成员函数
指针和引用区别
- 指针有自己的一块空间. 引用只是个别名
- sizeof(指针)=4/8字节, 取决于机器位数. sizeof(引用)=引用对象大小
- 指针可以被初始化为nullptr, 引用必须被初始化为一个已有对象的引用
- 可以有const指针, 没有const引用
- 指针可以有多级(**p), 引用只有一级
- 指针和引用使用++运算符意义不同, 前者
- 返回东爱内存分配的对象, 必须使用指针, 否则可能引起内存泄漏
- 指针可以任意改变指向对象, 引用一旦被初始化就不能改变指向指针
- 指针使用时需要解引用(->), 引用可以直接使用
结构体对齐
- 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 为编译器默认的一个对齐数与该成员大小中的较小值。vs中默认值是8 Linux默认值为4(当然可以通过#pragma pack()修改),但修改只能设置成1,2,4,8,16.
- 结构体总大小为最大对齐数的整数倍。(每个成员变量都有自己的对齐数)
- 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。
区别一下指针类型
int *p[10] //指针数组, 强调数组概念, 是一个数组, 每个元素都是指向int*
int (*p)[10] //数组指针, 强调指针概念, 只有一个变量, 指向int数组
int *p(int) //函数,返回值为int*
int (*p)(int)//函数指针,返回值为int
什么情况必须使用初始化成员列表
- 初始化const成员
- 初始化reference成员
- 调用基类的构造函数
- 调用数据成员对象的构造函数
strlen和sizeof区别
- sizeof是运算符, 而不是函数, 结果在编译时得到. strlen是C语言库函数
- sizeof参数可以是任何数据类型, strlen参数只能是字符指针且结尾是'\0'的字符串
- sizeof不能得到动态分配的变量的大小
常量指针和指针常量区别
- 常量指针是指向常量的指针: const char *str="zhanyi";
- 指针常量是指不能改变指向的指针: char *const str=p;
数组名和指针区别
- sizeof得到结果不同
- 字符数组内容可以改版, 字符指针内容不能改变
野指针和悬空指针
- 野指针: 指针指向了被delete/free的内存
- 空指针: 没有指向的指针
C和C++区别
- C++是面向对象语言, C是面向过程语言
- C++具有封装, 继承和多态三种特性
- C++相比C, 增加许多类型安全的功能
- C++支持方式编程, 不如模板函数和模板类
extern"C"用法
内联函数和宏定义区别
- 根本区别宏定义只是字符替换, 内联函数是个函数(类型安全检查, 自动类型转换)
- 代码展开发生在程序执行的不同阶段. 前者在预处理阶段, 后者在编译阶段
- 内陆函数可以作为类的成员函数, 宏定义不能
const和指针
- 顶层const: const修饰的变量本身是个常量, 即const在*右边
- 底层const: const修饰的变量指向的对象是个常量, const在*左边
- const_cast只对底层const起作用
define和const区别
- define是在预处理阶段起作用, const在编译, 运行阶段起作用
- define只做替换, 不做类型安全检查和计算, const相反
- define在内存中有多份相同的备份, const只有一份
#和##在define中的作用
assert
其他
C和C++的类型安全
- 类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域
- C的类型安全: C只在局部上下文中表现出类型安全, 比如试图从一种结构体的指针转换成另一种结构体的指针时, 编译器将会报告错误, 除非使用显式类型转换. 然而, C中相当多的操作是不安全的. 比如:
#include<stdio.h>
int main(){
printf("%d\n",10);//正常
printf("%f\n",10);//输出错误
printf("%s\n",10);//运行时segmentation
}
- 操作符new返回的指针类型严格与对象匹配,而不是void*
- C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
- 引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换
- 一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全
- C++提供了dynamiccast关键字,使得转换过程更加安全,因为dynamiccast比static_cast涉及更多具体的类型检查。
volatile关键字
- 提示编译器不要对访问该变量的代码进行优化
- 两个作用:保证变量的内存可见性;禁止指令重新排序
- 变量的内存可见性: 一个线程修改了某个变量, 对其他线程是可见的
explicit关键字
- 只能用于修饰只有一个参数的类构造函数
- 它的作用是表明该构造函数是显示的, 而非隐式的. 跟它相对应的另一个关键字是implicit, 意思是隐藏的, 类构造函数默认情况下即声明为implicit(隐式).
C++异常处理
- 用throw抛出异常, 在try作用域中运行可能抛出异常的代码, 通过catch捕捉异常
内存泄漏