cpp那些事儿

143 阅读9分钟

整理有关cpp的面试题目 参考

  1. C++ 那些事
  2. 阿秀的学习笔记

基本数据类型

image.png

大端和小端

大端,高位字节放在低位地址,低位字节放在高位地址。 小端,高位字节存高位地址,低位字节存低位地址。

举个例子,数值 0x12345678,其中 0x12 这一端是高位字节,0x78 这一端是低位字节。

int main() { 
    int a = 0x1234; 
    char *p = (char *)&a; 
    printf("%02x\n", *p);       //  读低地址
    printf("%02x\n", *(p + 1)); //  
    printf("%02x\n", *(p + 2)); 
    return 0; 
}

大端: 00,12 小端: 34

new 和 malloc区别

newmalloc
运算符函数
自动计算分配空间大小手工计算
类型安全返回值是指针,需要强制类型转换
会调用构造函数仅分配内存
失败bad_malloc失败抛出null

delete和free区别

delete会调用析构对象,free仅释放内存

c和cpp区别

  1. cpp里边new/delete,c里边malloc、free
  2. cpp允许函数重载
  3. cpp支持try,catch和throw异常处理机制

cpp内存

ELF

image.png

堆和栈的区别

管理手动分配操作系统自动分配
大小GB级别MB级别
方式动态分配静态动态都有
效率高,有专门的寄存器处理

malloc原理

参考malloc 的实现原理 内存池 mmap sbrk 链表

  1. 若分配内存小于 128k ,调用 sbrk() ,将堆顶指针向高地址移动,获得新的虚存空间。内存释放回到内存池。
  2. 若分配内存大于 128k ,调用 mmap() ,匿名映射(fd = -1)的方法在文件映射
  3. 区域中分配一块内存。这种内存释放的时候会直接还给操作系统。
void *mmap(void * addr , size_t length , int prot , int flags ,
                  int fd , off_t offset ); 
  1. malloc采用内存池,先申请一块大内存,切分成大小不同的内存块,用户申请选一块相近的内存块。
  2. 不是malloc之后立刻占用实际内存,第一次访问时产生缺页中断才分配物理内存。

分配方式:分离的空闲链表,维护多个空闲链表。特例是伙伴系统分配器的主要优点是它的快速搜索和快速合并。

free原理

free 只接受一个指针,却可以释放恰当大小的内存,这是因为在分配的区域的首部保存了该区域的大小。

深拷贝 vs 浅拷贝区别

深拷贝:深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。 浅拷贝:拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。

仿函数

仿函数就是一种具有函数特质的对象,需要重载函数运算符()sort(vec.begin(),vec.end(),less<int>())

const

用途

防止修改;节省空间;注:非const默认是extern,const要在其他文件中使用不许指定为extern。

const和define区别

  1. define在预处理展开,const是编译使用;
  2. const有类型,编译器可进行安全检查;
  3. define不能被调试;
  4. define有多份copy,const全局只有一份copy

指针与const

  1. 底层const:const在*左边,const char* a``char const * a

指针所指对象是常量,指向常量的指针
注意:

const int *ptr; 
// 可以不赋值,*ptr才是常量,ptr不是
// 不能通过指针*ptr 改对象值
// 必须用const void* 保存 const 对象地址
// 非const对象地址可以赋值给const对象指针
int val = 3;
*ptr = &val;
  1. 顶层const:const在*右边, char* const a 指针是常量,常量指针
int num = 0;
int* const ptr = &num;
// 必须初始化
// const常量不能赋值给常量指针
const int a = 10;
int * const ptr1 = &a; // error: const int * -> int *
const int * ptr2 = &a; // ok
const int * const ptr3 = &a; // ok

函数与const

  1. 引用传递不需要临时对象
  2. 函数:const对象只能访问const成员函数;非const对象访问任意成员函数,包括const成员函数
class A {
void getx () const;
void gety();
}
const A a;
a.getx(); //ok
a.gety(); //error
A b;
b.getx(); //ok
b.gety(); //ok
  1. 函数:const成员函数只能访问const成员函数,const成员变量
void
A::getx() const {
    gety(); //error
}
  1. 变量:类中的const成员变量必须通过初始化列表进行初始化

constexpr

  1. const是编译期或运行期常量(仅修饰成员函数)
  2. constexpr修饰变量,编译期就可知
  3. constexpr修饰函数,实参是常量能在编译器出结果就直接出,否则推迟到运行期

static

参考C/C++ 中的static关键字

静态局部变量

函数中静态局部变量:静态局部变量的空间只分配一次,函数再次调用会保留上次调用的值

void demo() 
{ 
	// static variable 
	static int count = 0; 
	cout << count << " "; 
	
	// value is updated and 
	// will be carried to next 
	// function calls 
	count++; 
} 

int main() 
{ 
	for (int i=0; i<5; i++)	 
		demo();  // 0 1 2 3 4 
	return 0; 
}

静态全局变量/函数

静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;因此可以防止命名冲突

静态成员变量

  1. 静态成员变量是所有对象共有的,静态成员变量只有一份;静态数据成员不占用类的内存,分配在全局数据区(在类外部初始化,类内部仅能声明不能初始化,静态数据的变量一般有默认初始化过程为0),没有类的实例就可以操作。
class A {
    static int a;
};
int A::a = 100;
int main()
{
    cout << sizeof(A) << endl;
}
  1. static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。

场景

同类的多个对象实现数据共享,节省存储空间。

静态成员函数

仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。
原因:没有this指针,不能被声明为const、虚函数和volatile,非static任意访问

静态成员函数为什么不能声明为虚函数?

static成员函数不属于任何对象或实例,加virtual没意义;虚函数需要this指针访问vptr,static成员函数没有this指针。

this

  1. this并不是对象一部分,不会影响sizeof结果
  2. 使用:非静态成员函数中返回类对象本身,return *this;;形参与成员变量名称相同用于区分。
  3. 顶层const,T * const this是非静态成员函数的第一个参数
A a;
a.func(10); // A::func(&a,10);
  1. this在成员函数的开始前构造,在成员函数的结束后清除

inline

  1. 编译时,函数体嵌入调用处
  2. 代码膨胀,仅节省了函数调用开销
  3. 虚函数可以是inline,前提是虚函数不表现为多态性;表现多态性的话不能知道运行期间调用哪个代码。
class Base

{

public:

virtual void who()

{

cout << "I am Base\n";

}

};

class Derived: public Base

{

public:

void who()

{

cout << "I am Derived\n";

}

};

int main()

{

// note here virtual function who() is called through

// object of the class (it will be resolved at compile

// time) so it can be inlined.

Base b;

b.who();

// Here virtual function is called through pointer,

// so it cannot be inlined

Base *ptr = new Derived();

ptr->who();

return 0;

}

define和inline的区别:

  1. 宏函数 预处理时候文本替换,内联函数 编译时嵌入\
  2. 内联函数有类型检查和语法判断 \
  3. 编译器可以拒绝不符合条件的内联函数,宏函数直接替换\
  4. 宏不能访问类的私有成员
    宏函数原地替换,遇到优先级不同可能带来问题,每个变量都带括号

sizeof

  1. 空类大小为1
  2. 虚函数本身,成员函数和静态成员不占存储
  3. 不管多少个虚函数,只有一个虚函数指针,vptr

虚函数

cloud.tencent.com/developer/a…

  1. 在运行时动态绑定,实现多态
  2. 虚函数表实现,虚函数地址保存在虚函数表vtable中,类的空间保存了虚函数指针vptr指向vtable。
  3. class单继承,每个class有自己的vtable,存在override就写入自己的虚函数地址,不存在就用父类对应的函数地址;其他的虚函数添加到vtable的末尾。
  4. class多(n)继承,派生类存在n个vptr,派生类自己的虚函数放在第一个虚函数表后边
  5. 虚函数表在编译期间创建,虚函数指针在运行时创建(构造函数自动加赋值函数)
  6. 动态联编:运行时才确定调用的函数;静态联编:编译时确定的函数调用关系。
  7. 虚函数调用取决于指向对象的类型;虚函数默认参数看指针本来的类型。默认参数是静态绑定的。
A a;  // 静态联编
a.fun();

A *b; // 动态联编 
b->fun();
class Base 
{ 
    public: 
        virtual void fun ( int x = 10) 
        { 
            cout << "Base::fun(), x = " << x << endl; 
        } 
}; 

class Derived : public Base 
{ 
    public: 
        virtual void fun ( int x = 20 ) 
        { 
            cout << "Derived::fun(), x = " << x << endl; 
        } 
}; 


int main() 
{ 
    Derived d1; 
    Base *bp = &d1; 
    bp->fun();  // 10
    return 0; 
} 
  1. 构造函数不能是虚函数,编译器自动在构造函数中创建vptr,构造函数是虚函数的话,需要vptr访问vtable,这个时候vptr还没产生。

overload override overwrite

  1. overload重载,同一个类中:参数不同(数目、类型、顺序),名字相同的函数
  2. override覆盖,派生类函数重写基类函数:名字相同,参数相同,基类有virtual限定
  3. overwrite隐藏,派生类隐藏基类函数:名字相同,参数不同无论基类virtual与否都是隐藏;参数相同基类没有virtual关键字。

多态

父类指针指向子类对象

纯虚函数

virtual void show() = 0;
  1. 只能作为基类来派生新类
  2. 不能创建抽象类的对象,但抽象类的指针可以指向派生类\抽象
    Derived d;
    Base &b = d;
    Base *ptr = new Derived();
  1. 构造函数不能是虚函数,析构函数(尽量)是虚函数

vtable

  1. 每个使用虚函数的类都有虚拟表
  2. 与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针

为什么析构函数要被设置成虚函数

class Shape {
    public:
    virtual void draw() = 0;
    virtual ~Shape() {
        cout << "~shape()" << endl;
    }
};
class Circle : public Shape{
    public:
    void draw() {
        cout << "draw" << endl;
    }
    ~Circle() {
        cout << "~circle()" << endl;
    }
};
int main() {    
    Shape* p = new Circle();
    delete p;
    return 0;
}

当父类指针指向子类对象的时候,释放父类指针,如果此时析构函数不是虚函数,那么将只会调用父类的析构函数,不会调用子类的析构函数,造成内存泄漏问题。

内存对齐

  1. 解决问题:内存对齐的主要目的是为了减少 CPU 访问内存的次数,加大 CPU 访问内存的吞吐量。
  2. static 成员变量不占用结构体的空间,由于静态成员变量在程序初始化时已经在静态存储区分配完成,所有该结构体实例中的静态成员都指向同一个内存区域;

强制类型转换

  1. const_cast 编译期完成,去除const/volatile
  2. static_cast 编译期完成,信息丢失需要程序员自己保证; 上行(派生类到基类)安全、下行不安全
  3. dynamic_cast 动态转换有类型检查,用于下行(基类转派生类)安全,可实现能否安全将对象指针转换为目标指针,转换失败(派生类到基类,不完整的对象)返回nullptr;上行等同于static_cast;

指针、引用转换到类

需要编译器的**Run-Time Type Information (RTTI)**支持

  1. reinterpret_cast 编译期完成,强制类型转换

volatile

  1. 不要优化
  2. 每次从内存取,不从寄存器取
  3. const可以有volatile

// 需要加上volatile修饰,运行时才能看到效果
const volatile int MAX_LEN  = 1024;

auto ptr = (int*)(&MAX_LEN);
*ptr = 2048;
cout << MAX_LEN << endl;      // 输出2048

const表示read only,需要编译器优化;volatile会禁止优化,因此可以被修改 4. 指针可以是volatile

运算符重载

  1. 前缀形式重载调用operator ++ () ,后缀形式重载调用 operator ++ (int)。

int 在括号内是为了向编译器说明这是一个后缀形式,而不是表示整数。

内存管理

智能指针

juejin.cn/post/684490…

  1. 利用RAII技术,构造分配,析构释放。
  2. 智能指针有一个通用的规则,就是->表示用于调用指针原有的方法,而.则表示调用智能指针本身的方法。

unique_ptr

独占指针,对象析构自动调用dalete。

shared_ptr

共享指针,多个指针指向同一个对象。它会在创建时分配一个引用计数,每次复制构造或赋值时,引用计数加一,每次析构时,引用计数减一,当引用计数变为零时,自动删除内存。

weak_ptr

解决 shared_ptr 的循环引用问题(A引用B,B引用A;将一方改为weak_ptr即可避免内存泄漏)。它不会增加引用计数,也不会影响对象的生命周期,当对象被释放时,weak_ptr 会自动变成空指针,因此 weak_ptr 不拥有对象的所有权。

问题

  1. 共享指针能否转为独占指针? 共享权限被撤销,会影响其他指向该对象的共享指针。
  2. 独占指针能否转为共享指针? 独占指针的所有权被释放,允许多个指针访问。

assert

  1. 断言是宏,不是函数
  2. NDEBUG可以关闭assert

extern

声明 和 定义

  1. 声明:告知编译器变量类型和存在性。
  2. 定义:分配内存初始化 一个变量或函数只能被定义一次,但可以被多次声明。

extern

分离式编译,程序可被分割成若干文件独立编译,需要文件间共享数据。

在一个源代码文件定义变量或函数,其他文件extern声明即可。

extern "C"

cpp文件编译后生成的符号与c语言生成的不同,如果C++中使用C语言实现的函数,在编译链接的时候,会出错,提示找不到对应的符号。

此时extern "C"就起作用了:告诉链接器去寻找C语言符号,而不是经过C++修饰的符号。

ifdef/endif 作用

  1. 条件编译,如#ifdef __cplusplus
#ifndef ADD_H
#define ADD_H
extern int add(int x,int y);
#endif
  1. 避免项目重定义:多个文件同时包含一个头文件,链接时会出现重定义报错。如a.h头文件包含了mongoose.h,而在b.c文件中#include a.h和mongoose.h)这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。