面经——C++语言1、2
主要参考《阿秀笔记》,次要参考博客和侯捷老师视频及C++ Primer
基本语法1
-
在main函数调用之前和之后执行的代码是什么?(侯捷——C++程序的生前死后)
- C++最开始执行一个_stdcall 格式的Entry-Point Symbol函数(也称为startup code),这个函数通常由编译器的设置,主要进行运行库和程序运行环境进行初始化
- 调用main之前执行的主要函数
- _heap_init(…):分配内存,构建堆栈
- ioinit():分配具有64个File结构体指针元素的静态数组,并初始化File结构体和静态数组
- 4个字符串处理函数:为main函数接受的命令行参数进内存分配并处理
- 调用可执文件和各动态库的构建s函数
- main函数执行完毕之后,返回到入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程
-
结构体内存对齐问题
-
结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
-
未特殊说明时,按结构体中size最大的成员对齐(若有double成员,按8字节对齐。)
struct alignas(4) Info2 { uint8_t a; uint16_t b; uint8_t c; };
-
-
指针和引用的区别
- 本质上:指针是一个存储地址的变量,引用是原来变量的别名(类似const指针)
- 层级上:指针可以有多级,引用只能有一级
- 初始化:指针可以为空,引用不能为空,在定义时必须初始化
- sizeof:指针式指针本身的大小,通常和寻址位数有关。引用得到的是所指向的变量大小
- 赋值上:引用初始化后不可改变指向,指针的声明和定义可以分开
-
在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?
- 类对象作为参数传递的时候要使用引用,这是C++类对象传递的标准方式
- 引用传递不需要创建临时变量,开销更小,适合栈空间大小敏感的使用
- 指针传参本质也是值传递(形参和实参相互独立),引用传参后对形参的任何操作都会间接寻址到实参
- 对于局部变量的引用没有意义
-
堆和栈的区别
- 申请方式不同
- 栈是系统自动分配的,也可以动态分配
- 堆是程序员申请和释放的,只能动态分配
- 大小不同
- 栈的大小是预设好的,通常向低地址生长。可通过ulimit -a查看和ulimit -s进行修
- 堆是向高地址生长的,是不连续的内存区域,大小可以灵活调整
- 申请效率不同
- 栈由系统分配,速度快,无碎片
- 堆由程序员分配,速度慢,有碎片
- 内存管理方式
- 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序
- 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出
- 效率
- 堆是函数库提供的,分配内存需要通过算法运算,通常效率低
- 栈是系统级的数据结构,栈操作都有专门的指令,效率较高
- 申请方式不同
-
new和delete是如何实现的?
-
new的本质:调用operator new分配内存,将该内存进行转型后赋值给指针,然后调用构造函数进行赋值
Complex *pc = new Complex(1, 2); // 用c语言进行其编译功能的解释 Complex *pc; try{ //分配内存,实质调用malloc void *mem = operator new(sizeof(Complex)); // 内存转型,赋值给指针 pc = static_cast<Complex*>(mem); // 调用构造函数实例化内存(赋值) pc->Complex::Complex(1, 2); }catch(std::bad_alloc){ // 若allocation失败就不执行constructor }
-
operator new的本质:使用malloc进行分配内存,同时进行异常处理。可以重载
// 第二参数保证函数不抛出异常 void *operator new(size_t size, const std::nothrow_t &){ void *p; // 如果内存耗尽导致分配失败 (实质调用malloc) while((p=malloc(size)) == 0){ _TRY_BEGIN if(_callnewh(size) == 0)// 调用自定义函数进行处理 break; _CATCH(std::bad_alloc) return 0; _CATCH_END } return p; }
-
delete的本质:先调用析构函数处理类对象,后调用operator delete函数进行释放。operator delete函数本质是调用free函数
delete pc; // 使用c语言进行翻译 pc->~Complex();// 先析构对象 operator delete(pc);// 后释放 //operator delete源码 void __cdecl operator delete(void *p)_THROW0(){ free(p); }
-
-
区别以下指针类型
int *p[10]; // 指针数组,数组内的每个元素int类型的指针 int (*p)[10]; // 数组指针,是一个指向具有10个元素的数组的指针变量 int *p(int); // 函数声明,函数名是p,参数是int类型的,返回值是int*类型的 int (*p)(int); // 函数指针,指向参数为int类型并返回值也为int类型的函数
-
new / delete 与 malloc / free的异同
相同点
- 都用于内存的动态申请和释放
不同点
-
根本不同:new/delete底层使用的是malloc/free,还封装了对象创建时候的构造函数,和销毁的时候要执行的析构函数。被free回收的内存是立即返还给操作系统吗?
不是的,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片
-
malloc和free是标准库函数,支持覆盖。new和delete是运算符,支持重载
-
空间分配上,new可以自动计算空间分配大小,malloc需要手工计算
-
安全性上,new是类型安全的,malloc不检查分配内存是否可以被内存直接使用
-
malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
-
new和delete实现不同,见上
-
被free回收的内存是立即返还给操作系统吗?
不会,被free回收的内存会首先被ptmalloc使用双链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。避免了频繁的系统调用,提高系统资源利用率。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片
-
宏定义和函数有何区别?
- 宏定义在在预编译阶段完成替换,执行效率高,但是增加了代码量
- 函数调用会进行类型检查和堆栈操作,效率慢
- 宏定义通常用于系统底层接口参数和功能的抽象
-
宏定义和typedef的区别
- 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
- 宏替换发生在编译阶段之前,属于文本插入替换;typedef是编译的一部分。
- 宏不检查类型;typedef会检查数据类型。
- 宏不是语句,不在在最后加分号;typedef是语句,要加分号标识结束。
-
变量声明和定义的区别
- 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。
- 相同变量可以在多处声明(外部变量extern),但只能在一处定义。
-
strlen和sizeof区别?
- sizeof是运算符,结果在编译时得到而非运行中获得。strlen是字符处理的库函数
- sizeof参数可以是任何数据的类型或者数据。strlen的参数只能是字符指针且结尾是’\0’的字符串。
-
一个指针占的字节数?
- 一个指针占内存的大小跟编译环境有关,而与机器的位数无关
64位处理器上64位操作系统的32位编译器,指针大小8字节。
64位处理器上32位操作系统的16位编译器,指针大小4字节。32位处理器上32位操作系统的32位编译器,指针大小4字节。
32位处理器上32位操作系统的16位编译器,指针大小2字节。32位处理器上16位操作系统的16位编译器,指针大小2字节。
16位处理器上16位操作系统的16位编译器,指针大小2字节。 -
常量指针和指针常量区别?
-
指针常量是指向常量的指针,
int const *p / const int *p
-
常量指针:指针是个常量,不能改变指向,必须初始化且之后不能改变
int *const p
-
-
int a[10];int (*p)[10];
a和&a有什么区别?- a是数组名,也是数组首元素地址。+1表示数组下一个元素的首地址
- &a数组的指针,类型为
int(*)[10]
,+1表示数组首地址加整个数组的偏移量,即数组末尾下一个元素的地址
-
C++和Python的区别
- 执行方式上:python是一个脚本语言,是解释执行的,更容易跨平台,但是效率低一些。C++是编译语言,效率高。
- 代码风格上:python语法简洁,使用缩进来区分不同的代码块,C++用花括号进行区分
- python库函数比C++多,调用方便
-
C++和C语言的区别
- C++中new和delete是对内存分配的运算符,取代了C中的malloc和free。
- 标准C++中的字符串类取代了标准C函数库头文件中的字符数组处理函数
- C++中用来做控制态输入输出的iostream类库替代了标准C中的stdio函数库。
- C++中的try/catch/throw异常处理机制取代了标准C中的setjmp()和longjmp()函数。
- C++可以重载,C语言不允许。
- 在C++中,除了值和指针之外,新增了引用
- C++相对与C增加了一些关键字,如:bool、using、dynamic_cast、namespace等等
基本语法2
-
C++和Java的区别
- 编译上:Java源码可以一次编译,然后在不同平台的JVM上翻译对应的机器码运行,实现了良好的跨平台特性。C++一次性编译链接直接生成机器码,性能高但跨平台能力差。
- 指针上:C++可以通过指针直接进行内存的操作,效率高但是不安全。Java使用自动的内存管理机制,减少了指针操作失误,但JVM内部仍然使用的是指针,不能杜绝内存泄漏的问题。
- 多重继承:C++支持多重继承,允许多个父类派生一个类,功能强但是使用复杂。Java不支持多重继承,但是允许一个类继承多个接口,减少继承多个实现的父类带来的复杂性
- 内存管理机制:Java使用自动的内存管理机制,JVM可以自动进行无用内存的释放。C++没有垃圾回收机制,需要手动释放申请的堆内存
- 操作符重载:Java不支持操作符重载,是C++的突出特征
- 字符串:Java具有类对象实现的字符串,统一和简化了操作
- 类型转换:C++具有隐式的自动转换,但是Java只能由程序进行强制类型转换
-
C++中struct和class的区别
- 属性上:如果对成员不指定struct 默认是公有,class默认是私有
- 继承上:struct默认是公有继承,class默认是私有继承
- C++保留struct关键字的原因是兼容C(成员是public),但是C不能进行成员函数的定义
- class还可以定义模板参数,但是关键字struct不能用于定义模板参数
-
define宏定义和const的区别
- 作用域:define发生在预编译阶段进行文本替换,const作用于编译过程
- 安全性:define不进行类型检查,最好使用大括号包含替换的内容。const具有数据类型,编译器会进行类型的安全性检查
- 内存占用:宏定义数据没有分配内存空间。const定义的变量值不能变,但要分配内存空间
-
C++中const和static的作用
static关键字:
-
修饰全局变量时,表明一个全局变量只对定义在同一文件中的函数可见。
-
修饰局部变量时,在全局数据区分配内存,不会因函数终止而丢失
-
修饰函数时,表明该函数不能被其他文件所用,而其他文件可以定义同名函数
-
修饰类的数据成员,表明该实例归所有该类对象共有。
-
修饰类内的函数,该函数可以被非静态函数访问,但是它只能访问静态的函数和数据
const关键字:
- const变量在定义时必须进行初始化,之后无法更改且在其他文件无法使用
- const形参可以接受const和非const类型的形参
- const成员变量,只能通过构造函数初始化列表进行初始化
- const成员函数,不能调用非const成员函数,但可以被非const成员函数调用
-
-
C++的顶层const和底层const
- 顶层const:const修饰的变量本身无法修改,指的是指针,就是 * 号的右边
- 底层const:const修饰的变量所指向的对象是一个无法修改,就是 * 号的左边
- 总结:const总是修饰的是其后面的整体
// 顶层const,表示b1是常量 const int b1 = 20; int const b1 = 20; // 顶层const,表示b1是常量 int a = 10; int *const b2 = &a; // 底层const,b3可变,但是指向的对象不可变 const int *b3 = &a; // 底层const,引用变量不可变 const int &b5 = a;
-
数组名和指向数组首元素的指针区别?
- 数组名不是真正意义上的指针,没有自增、自减等操作。
- 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小
-
final和override关键字
-
final:修饰类名表示该类不会被继承,修饰虚函数表明该函数不能被重写。如果被继承或重写,编译器会报错
class Base { virtual void foo(); }; class A : public Base { void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写 }; class B final : A // 指明B是不可以被继承的 { void foo() override; // Error: 在A中已经被final了 }; class C : B // Error: B is final { };
-
override:声明该函数时重写的父类的虚函数,如果不是父类函数会报错,防止写错
class A { virtual void foo(); }; class B : public A { virtual void f00(); //OK,这个函数是B新增的,不是继承的 virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错 };
-
-
用于类对象的直接初始化和拷贝初始化
详细解释的博客-
直接初始化:直接调用与实参匹配的构造函数,通常是"( )"赋值的形式
-
拷贝初始化:总是调用拷贝构造函数,会先调用构造函数创建临时对象,后调用复制构造函数用改临时对象初始化,通常是" = "赋值的形式
string str1("I am a string");//语句1 直接初始化 string str2(str1);//语句2 直接初始化 string str3 = "I am a string";//语句3,拷贝初始化 string str4 = str1;//语句4,拷贝初始化
- 编译器会进行拷贝初始化的优化,但是以下几种情况只能使用直接初始化
- 拷贝构造函数是private
- 使用explicit修饰构造函数
-
-
extern "C"
的用法- 作用:告诉C++编译器该部分代码使用C语言进行编译,只能放在cpp和h文件中,将C与C++桥梁代码包裹起来
// 1. C++调用C函数 //xx.h extern int add(...) //xx.c int add(){ } //xx.cpp extern "C" { #include "xx.h" } // 2. C调用C++函数 //xx.h extern "C"{ int add(); } //xx.cpp int add(){ } //xx.c extern int add();
-
野指针和悬空指针
- 野指针:未被初始化的指针,访问行为不可控。未使用的指针初始化应赋值为
nullptr
,这样在使用时编译器会报错,避免非法访问 - 悬空指针:指针最初指向的指针被释放。指针释放后用其赋值的指针应该置空。C++的智能指针可以避免悬空指针的出现
- 野指针:未被初始化的指针,访问行为不可控。未使用的指针初始化应赋值为
-
类型安全
- 定义:类型安全的代码只能访问被授权的内存区域,若无强制类型转换则会报错
- C++的类型安全机制
- 操作符new返回的指针类型严格与对象匹配
- 模板函数支持类型检查
- const关键字进行作用域定义
- 使用inline和函数重载,可在类型安全的情况下支持多种类型
- 提供dynamic_cast关键字进行更强的类型检查
- 解决方法:减少强制类型转换和空指针类型void*的使用
-
C++中重载、重写(覆盖)和隐藏的区别
- 重载:
- 函数参数的个数、类型或顺序存在不同,但函数名相同。
- 函数类型也可变,但不能只重载函数类型。
- 重写:
- 在派生类中覆盖基类中的同名函数
- 重写的基类函数必须是虚函数,且与基类虚函数具有相同的参数列表和返回值类型
- 隐藏:
- 派生类函数声明与基类同名的函数,但是基类函数不是虚函数
- 重载:
-
C++有哪几种的构造函数
- 默认构造函数(无参数):
- 定义类的对象时,没有提供初始化式就会调用类的默认构造函数
- 由编译器创建的构造函数是合成的默认构造函数,这种合成可能失败
- 为所有形参提供实参初始化的构造函数是自定义的默认构造函数
- 初试化构造函数:
- 使用实参在创建对象时为对象的成员属性赋值,由编译器自动调用
- 拷贝构造函数
- 用于将对象作为函数参数、返回值或初始化其他对象时调用,进行深拷贝后传递
- 如果没有定义拷贝构造函数,编译器会自行定义。
- 移动构造函数
- 临时对象转移内存所属权时调用,使用右值引用作为参数
- 委托构造函数
- 类中往往有多个构造函数,只是参数表和初始化列表不同,其初始化算法都是相同的。为了避免代码重复,可以使用委托构造函数
- 转换构造函数
- 只有一个参数的构造函数,而且该参数又不是本类的const引用
#include <iostream> using namespace std; class Student{ public: //1. 默认构造函数,没有参数 Student(){ this->age = 20; this->num = 1000; }; // 2. 初始化构造函数,有参数和参数列表 Student(int a, int n):age(a), num(n){}; // 3. 拷贝构造函数,参数是对象 Student(const Student& s){ this->age = s.age; this->num = s.num; }; // 4. 移动构造函数,参数是右值引用 Student(const Student&& s){ this->age = s.age; this->num = s.num; }; // 3. 转换构造函数,形参是其他类型变量,且只有一个形参 Student(int r){ // this->age = r; this->num = 1002; }; ~Student(){} public: int age; int num; };
- 默认构造函数(无参数):
-
浅拷贝和深拷贝的区别
- 浅拷贝:拷贝的是内存对象的地址,而不是内容。即多个指针指向同一个内存对象,如果其中一个释放了该内存对象,其他浅拷贝指针会出现错误
- 深拷贝:拷贝内存对象到一个新的内存区域,此时存在多个相同的内存对象。即使原来对象析构了,也不影响拷贝的新对象。
-
内联函数和宏定义的区别
- 执行时间:宏只在编译前进行简单的字符串替换,内联函数在编译时进行类型检查,将代码嵌入目标代码中,省去函数调用开销来提高效率,有返回值也可以重载
- 书写形式:宏尽量括号括起来,否则容易出现歧义。内联函数和普通函数一样,使用宏的地方都可以使用内联函数
-
public,protected和private访问和继承权限的区别
修饰变量和函数:
- public:在类的内部外部都可以访问。
- protected:只能在类的内部和其派生类中访问。
- private:只能在类内访问
权限继承:
- public继承:基类中各成员属性保持不变,基类中private成员被隐藏,派生类不能访问
- protected继承:基类中各成员属性均变为protected,基类中private成员被隐藏,派生类不能访问
- private继承:基类中的各成员属性全变成private,基类中private成员被隐藏,派生类不能访问
派生类对基类的访问
- 内部访问:由派生类中新增的成员函数对从基类继承来的成员的访问
- 外部访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问
- 除了public继承的public其他外部访问均不可
-
如果用代码判断大小端存储
-
大端存储:字数据的高字节存储在低字节
-
小端存储:字数据的低字节存储在低地址中
-
注意:在Socket编程中,往往需要将操作系统所用的小端存储的IP地址转换为大端存储,这样才能进行网络传输
-
-
volatile、mutable和explicit关键字的用法
volatile关键字
- 修饰类型变量,编译器将不在对该变量代码进行优化,从而提供稳定的访问
- 声明变量的值,系统总是重新从它所在的内存中读取数据,而不是使用寄存器的备份
- 赋值上,不能把非volatile对象赋给一个volatile对象,其他可以
- 修饰类,C++中一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。
- 多线程下,防止优化编译器把变量从内存装入CPU寄存器中。如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,这会造成程序的错误执行
mutable
- 在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置。
- const对象不能调用非const对象,但是加上mutable便可以访问
explicit
- explicit关键字用来修饰类的单参数的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换
-
什么情况下调用拷贝构造函数
- 通常是在类使用其他对象初始化、作为函数参数传递或者函数返回值是类对象时,但是g++中函数返回局部对象不会引发拷贝构造