面试会问到的知识点
1.结构体对齐
默认按照最大字节数据对齐。例如:
struct Info {
short a; // 2字节
int b; // 4字节
long c; //(32位编译器4字节,64位编译器8字节)
long long d; // 8字节
}
//对齐方式按照最长8字节,故而sizeof(Info) == 4*8 == 32
alignas可以指定对齐方式
struct alignas(4) Info {
short a; // 2字节
char c; // 1字节
}
//对齐方式按照4字节,故而sizeof(Info) == 2*4 == 8
alignas失效:
struct alignas(2) Info {
int a; // 4字节
char c; // 1字节
}
//指定对齐方式小于int的4字节,对齐方式依然为4字节,故而sizeof(Info) == 2*4 == 8
单字节对齐方式:
#if defined(__GNUC__) || defined(__GNUG__)
#define ONEBYTE_ALIGN __attribute__((packed))
#elif defined(_MSC_VER)
#define ONEBYTE_ALIGN
#pragma pack(push,1) //指定单字节对齐
#endif
struct Info {
uint8_t a;
uint32_t b;
uint8_t c;
} ONEBYTE_ALIGN;
#if defined(__GNUC__) || defined(__GNUG__)
#undef ONEBYTE_ALIGN
#elif defined(_MSC_VER)
#pragma pack(pop)
#undef ONEBYTE_ALIGN
#endif
std::cout << sizeof(Info) << std::endl; // 6 1 + 4 + 1
std::cout << alignof(Info) << std::endl; // 6
2.指针和引用
- 指针是一个变量,存储的是一个地址, 引用跟原来的变量实质上是同一个东西,是原变量的别名
- 指针可以为空(
NULL(c++11后用nullptr代替)),引用不能为空 - 指针可变,引用不可变
sizeof得到指针本身大小(32位编译器指针4字节,64位编译器8字节),而得到引用指向的变量大小。- 指针参数传递是实参的拷贝形参,修改后不会影响实参指向, 而引用参数传递是实参的地址,修改指向内容的值会影响原来的值。
3.堆和栈的区别
| 区别 | 堆 | 栈 |
|---|---|---|
| 申请方式 | 程序员代码分配和释放,容易产生内存泄漏 | 由系统自动分配 |
| 申请大小 | 大小可以灵活调整(默认1G-4G) | 大小固定(默认4M) |
| 申请效率 | 堆由程序员分配,速度慢,且会有碎片 | 栈由系统分配,速度快,不会有碎片 |
| 扩展方向 | 堆向高地址扩展,是不连续的内存区域 | 栈顶和栈底是之前预设好的,栈是向栈底扩展 |
| 分配方式 | 动态分配 | 有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。 |
| 使用方法 | 堆由C/C++函数库提供,机制很复杂 | 栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令 |
栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
4.new delete和malloc free
-
malloc和free是标准库函数,支持覆盖;new和delete是运算符,支持重载。
new在c++里封装了malloc,分配足够的内存空间并调用构造函数,返回对象指针,delete封装了free,不仅释放内存,而且还会调用析构函数。free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。避免了频繁的系统调用,ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
5.常量指针和指针常量
- 指针常量是一个指针,读成常量的指针,指向一个只读变量的指针
int const *p; // 指针常量
const int *q; // 指针常量
- 常量指针是一个不能改变指向的指针。
int *const p; // 常量指针
6.struct和class的区别
相同:
- 两者都拥有成员函数、公有和私有部分
struct可以实现class所有工作
不同:
- 不指定权限时,成员函数和成员变量在
class中默认是private,struct中默认是public class默认是private继承, 而struct默认是public继承
c和c++的struct
| 区别 | c | c++ |
|---|---|---|
| 类型 | 用户自定义数据类型(UDT) | 抽象数据类型(ADT),支持成员函数的定义 |
| 权限 | 没有权限的设置 | 有权限设置 |
| 声明对象 | 必须在结构标记前加上struct,才能做结构类型名来声明定义对象(除:typedef struct class{};) |
可以直接作为结构体类型名使用 |
7.const,static
(1)与类无关时
| 区别 | const | static |
|---|---|---|
| 初始化 | 定义时必须初始化且无法再改变 | 定义时不指定值,默认为0 |
| 可见性 | 和其他变量一样 | 全局变量和函数用了static只能在该文件所在的编译模块使用,否则全局可见 |
| 函数内 | 不可改变 | 只初始化一次,函数执行完毕也不回收内存 |
其中static的可见性解释也就是:使用include包含文件后也不能使用被包含文件中static声明的变量和函数
(2)与类有关时
| 区别 | const | static |
|---|---|---|
| 变量关联 | 和一般成员变量相同 | 只与类关联,不与类的对象关联 |
| 变量初始化 | 不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化 | 不能在类声明中初始化,必须在类定义体外部初始化 |
| 成员函数 | const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值 |
不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问 |
const修饰变量是也与static有一样的隐藏作用。只能在该文件中使用,其他文件不可以引用声明使用。 因此在头文件中声明const变量是没问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突
8.顶层const和底层const
- 顶层
const:const本身是一个常量,无法修改。(例如常量指针) - 底层
const:const修饰的变量指向的对象是常量,所指的对象。(例如指针常量)
9.final,override,virtual
class Father{
public:
Father(){ //构造函数一般不能为虚函数,无法用virtual修饰
cout<<"Construct Father!"<<endl;
}
virtual void func1(){
cout<<"func1 of Father!"<<endl;
}
void func2(){
cout<<"func2 of Father!"<<endl;
}
virtual void func3() = 0; //纯虚函数,父类就变成了虚基类,子类必须重写这个函数,如果不重写就会报错
virtual ~Father(){ //析构函数,一般都使用virtual修饰
cout<<"Destruct Father!"<<endl;
}
};
class Child : public Father{
public:
Child(){ //构造函数一般不能为虚函数,无法用virtual修饰
cout<<"Construct Child!"<<endl;
}
void func1() override { //继承自父类,不报错
cout<<"func1 of Child!"<<endl;
}
//void func0() override; //父类没有,会报错
void func2(){
cout<<"func2 of Child!"<<endl;
}
void func3() override {} //重写父类的纯虚函数
~Child(){
cout<<"Destruct Child!"<<endl;
}
};
int main(){
Father* f = new Child();
f->func1();
f->func2();
delete f;
return 0;
}
其中,虚基类无法定义对象,但可以定义为子类的指针。
上述main函数中的输出如下:
Construct Father! //在构造子类对象时,先调用父类构造函数
Construct Child! //再构造子类构造函数
func1 of Child! //父类的func1因为用了virtual被隐藏
func2 of Father! //父类的func2不是虚函数,未被隐藏
Destruct Child! //在析构指针时,先调用子类析构函数
Destruct Father! //再调用父类析构函数
注意,如果父类析构函数不是虚函数,析构父类指针时无法调用子类的析构函数。
10.拷贝初始化和直接初始化
class A{
public:
int num1;
int num2;
public:
A(int a=0, int b=0):num1(a),num2(b){}; //-直接构造函数
A(const A& a){}; //拷贝初始化函数
//重载 = 号操作符函数
A& operator=(const A& a){
num1 = a.num1 + 1;
num2 = a.num2 + 1;
return *this;
};
};
int main(){
A a(1,1); //直接初始化,调用构造函数
A a1 = a; //拷贝初始化操作,调用拷贝构造函数
A b;
b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}