c++

185 阅读14分钟

github.com/huihut/inte…

语法

const

作用:

  1. 修饰变量
  2. 修饰指针,指向常量的指针(UINT8 * const var)和常量指针(const UINT8 * p)前者表示 const 修饰的是指针表示指针存储的内存地址不能修改,后者表示内存地址存放的值是常量。助记: *代表地址 const 代表内容 谁在前代表谁不能被修改。指针常量的好处可以增加代码的可靠性。
  3. 修饰引用,指向常量的引用。用于形参类型,避免拷贝,避免函数对值得修改。引用得作用就是避免拷贝。
  4. 修饰成员函数,表明成员函数内不能修改成员变量。 double real() const {} 注意 const 位置要放在 小括号后面,大括号前面。表示不改变函数里面的内容。

const 的指针和引用

class A
{
private:
    const int a;                // 常对象成员,可以使用初始化列表或者类内初始化

public:
    // 构造函数
    A() : a(0) { };
    A(int x) : a(x) { };        // 初始化列表

    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
};

void function()
{
    // 对象
    A b;                        // 普通对象,可以调用全部成员函数
    const A a;                  // 常对象,只能调用常成员函数
    const A *p = &a;            // 指针变量,指向常对象
    const A &q = a;             // 指向常对象的引用

    // 指针
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指针变量,指向字符数组变量
    const char* p2 = greeting;          // 指针变量,指向字符数组常量(const 后面是 char,说明指向的字符(char)不可改变)
    char* const p3 = greeting;          // 自身是常量的指针,指向字符数组变量(const 后面是 p3,说明 p3 指针自身不可改变)
    const char* const p4 = greeting;    // 自身是常量的指针,指向字符数组常量
}

// 函数
void function1(const int Var);           // 传递过来的参数在函数内不可变
void function2(const char* Var);         // 参数指针所指内容为常量
void function3(char* const Var);         // 参数指针为常量
void function4(const int& Var);          // 引用参数在函数内为常量

// 函数返回值
const int function5();      // 返回一个常数
const int* function6();     // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7();     // 返回一个指向变量的常指针,使用:int* const p = function7();

引用

给变量起别名,新的变量指向原变量内存。引用必须初始化(引用依赖于变量),一旦初始化后就不可以修改。传给函数的参数是引用,相当于把原来的变量传进去,并且函数能够对其进行修改。

引用作为函数的返回值:a)不能返回局部变量,局部变量保存在栈上,如果第一次对是因为编译器做了保留,第二次不对是因为编译器释放了这块内存;b)如果函数返回类型是一个引用,那么函数本身可以充当左值。

常量引用:

  1. const int &b = 10 但是不能 int &b = 10
  2. const int &b = a; 限制 b 不能被修改,把 b 变成一个常量防止误操作。

引用的本质就是一个指针常量:

1,int &b = a ,内部会创建一个 int * const b = &a 的指针常量

2,b = 20; 内部发现 b 是一个指针常量会转换成 *b = 20;

到底是怎么转换的呢?

-> 函数传参

  • 值传递

  • 地址传递

  • 引用传递

    后面两种都会修饰实参,也就是会改变原来内容。值传递相当于复制了一个变量。

初始化列表

class A {
public:
  A(default a = 0, default b = 0)
    : pa (a), pb (b)   -> 叫做初始化列表
  {}
private:
  int pa, pb;
}

好处是直接初始化私有变量;如果放到花括号里面做,就会放弃在初始化的机会,而在赋值的时候操作,性能不好,并且不够大气。

代码段 数据段

protected

派生类只能通过派生类对象或者友元来访问基类受保护属性,但是派生类不能通过基类对象来访问基类受保护属性

static_cast

是一个显式转换的名称。 Cast_name(expression), cast_name 包括:

  • static_cast:显式转换,会关闭编译器告警。对于编译器无法自动执行的类型转换比较有用,可以找回 void * 指针中的值
  • Dynamic_cast:支持运行时类型识别
  • Const_cast:只能改变常量的属性,可以理解为去掉常量属性。 const char *c; char *p = const_cast<char*>(p); 但是 const_cast<string>(cp)就不对,因为不能改变类型,只能改变常量属性。
int main()
{
    const int a = 10;
    int &c = const_cast<int&>(a);
    c = 20;
    cout << a << " : " << c << endl;
}
为什么这里的 a 仍然是 10 呢?
  • reinterpret_cast:为运算对象的位模式提供较低层次上的

左值 右值

左值的意思是需要在等号左边有一个变量来接收结果;右值的意思是本身就是结果 ?

友元

  • 对于有些私有变量,外部不能访问。但是定义了一些友元的话,(友元指的是函数),可以直接访问该私有变量。

坏处就是打破了封装的大门。

  • 相同 class 的各个 objects 互为友元

static

  1. 修饰普通变量:修改存储区域和生命周期。存储在静态区,并且变量在main函数运行前就分配了空间。如果有初始值就用初始值初始化,否则使用系统默认值初始化。
  2. 修饰普通函数:限定该函数只能在当前文件内使用。防止与他人命名空间函数重名。
  3. 修饰成员变量:所有对象都保存一个该变量,并且不需要生成对象就可以访问该变量。
  4. 修饰成员函数:不需要生成对象就可以访问 static 修饰的成员函数,但是 static 函数内不能访问非静态成员

this 指针

this 指代调用的那个对象

class CMyDoc : public CDocument {
  virtual Serialize() {}
};
CDocument::OnFileOpen() {
  ...
  Serialize();
  ...
}
virtual Serialize();

main() {
  CMDoc myDoc;
  ...
  myDoc.OnFileOpen();
}

上面 myDoc.OnFileOpen(); 这个行为代表子类调用父类函数,通过命名空间调用到父类函数,然后将子类自身传入CDocument::OnFileOpen(&myDoc); 这里的&myDoc 就是 this。

所有的调用动作都是通过 this 来调用。然后父类中的虚函数 Serialize() 是通过 this->Serialize(); 来调用,这时候 this 指代的是子类对象。因此要去 vtbl 中去查找。(*(this->vptr)[n])(this); vtbl 就是动态绑定的原理。

函数指针

函数本身也是有地址的,可以将函数的地址当做参数传给另一个函数。

函数指针的形式:

函数返回类型   函数指针名   参数列表
double				(*pf)				(int);
注意:这里 *pf 必须要加括号,因为 () 优先级高于*
  *pf() 表示 pf()是一个返回指针类型的函数;
  (*pf)() 表示 pf 是一个指向函数的指针;

double pam(int); // 函数原型

pf = pam; 将函数地址赋值给函数指针。

函数指针的使用

函数指针可以理解为函数名,因此可以
 double a =  (*pf)(5);
或者
  double a = pf(5);
不过上面的形式更好,提醒是一个函数指针。

好处:

  • 可以在不同的时候调用不同的函数实现/函数(函数实现逻辑不同,但是对于函数原型一样)。

C++ 类和 C 的不同

  • C中的函数是为了处理不同类型的数据,但是语言没有提供关键字,导致数据是全局的,因此各个函数都可以处理。
  • C++将数据和处理数据的函数包在一起,该数据只有包在一起的函数可以处理它。提供了很多关键字来处理数据。

Class 的分类

  • 带指针的类
  • 不带指针的类:这种类不用写析构函数

inline 内联函数

如果函数在类中就定义了,这就是 inline 函数。最终是否是 inline 由编译器决定,如果函数很短,大概率是 inline

虚函数 vs 纯虚函数

虚函数的主要作用是让子类重写。子类重写的时候会在 () 后面加 override。

如果需要动态绑定某个函数,但是不用virtual 关键字是不行的。 因为编译器根据关键字来决定函数的解析是在编译时还是运行时。

子类必须将声明为virtual 的函数重写/重新声明

  • 虚函数是为了调用子类的函数。这个函数是实现的,即使是空实现,作用是让这个函数能够在子类被重写。方便编译器使用后期绑定来实现多态。

  • 虚函数在子类中可以不重写,但是纯虚函数在子类中必须实现才可以实例化子类,否则子类依旧是抽象类。纯虚函数所在的类叫做抽象类 virtual func() = 0;

  • 继承虚函数的类,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类完成。

  • 如果存在虚函数,构造函数会记录虚函数表的地址并且保存在各个实例里面

  • 派生类的构造函数会夹带调用基类的构造函数

    知乎上的阿布讲的挺好,而且用了 compiler explorer 工具,我大为震撼 www.zhihu.com/question/35… 能够直接从汇编语言去理解构造函数的原理。

虚析构函数和纯虚析构函数

  • 多态,若子类开辟了堆内存,父类无法直接调用子类的析构函数去释放内存。
  • 将父类中的析构函数改为虚析构函数或者纯虚析构。纯虚析构表示类为抽象类,无法实例化对象。

virtual func(){} virtual func() = 0;

  • 纯虚析构,不仅需要声明,也需要实现,不然代码无法运行。

为什么基类定义成虚析构函数就能够正确地对派生类对象进行析构

volatile

assert

sizeof

#pragma pack(n)

位域

extern “C“

struct 和 typedef struct

union

C++ 类

explicit 关键字

主要用于防止隐式转换。

具体点:

1,explicit 修饰构造函数的时候,可以防止隐式转换 和 复制初始化

2,explicit 修饰转换函数的时候,防止隐式转换。

//复制初始化
class A {
  A(int){}
  operator bool() const {return true;}
};
A a1 = 1; // 这时候会将 1 先初始化为 A 的某个对象,然后赋值给 a1

列表初始化。

C++ 内存模型

  1. 代码区:存放函数的二进制代码,共享同一份二进制文件且只读
  2. 全局区:存放全局变量,静态变量以及常量
  3. 堆:程序员分配和释放
  4. 栈:编译器自动分配和释放,存放函数参数值以及局部变量

不同的存放区域赋予不同的生命周期

C++ 定义运算符

vector 中的 [] 运算符定义

reference operator[](size_type n) {return *(begin() + n);}

学习方法论

以客户端代码为引导,观察所得结果并且证实源代码。

list 和 vector 迭代器的区别

  • list 的迭代器在插入操作后不会失效,但是 vector 在插入后很有可能导致整体存储体重新配置(扩容后,复制到新的空间,原来的空间废弃)从而导致原有的迭代器失效。

private vs protected

private 限制更严格,子类不能访问父类的 private 变量

protected 子类能够直接访问父类的 protected 变量。

面经给的不是具体的答案,而是面试考察的重点。具体在面试中的表现水平还是看平时的积累,光看八股文是讲不明白知识点的。只有靠自己深挖才行。

C++能否在有参构造函数中调用无参构造函数,无参构造函数中如果有修改类成员会不会对当前正在构造的类产生影响,这种调用方式有什么优势或者缺点。

C++的构造函数中能不能调用虚函C++的构造函数中能不能调用虚函数数

指针和引用的区别,你更倾向于使用指针还是引用,为什么

C++vector插入和删除为什么会导致迭代器实效

C++的左值和右值,怎么使用,有什么区别,std::move()函数(std::move()的实现原理)

程序的编译连接,静态链接和动态链接,分别什么时候链接的

虚函数与纯虚函数的区别

C++多态,重载重写

虚函数表和虚函数指针存放在那个位置

派生类的虚函数表与基类的虚函数表是同一个虚函数表吗,子类重写的虚函数怎么覆盖基类的

面向过程和面向对象各自的优缺点

内存屏障,volatile作用,是否具有原子性,使用volatile会对编译器有什么影响

可执行文件的文件格式(ELF文件格式)

struct字节对齐的规则

智能指针的作用(几种只能指针要非常熟悉)

C++中的内存泄漏(举例几个内存泄漏的场景)

怎么实现一个线程池

C++的构造析构顺序

构造函数能否是虚函数

不能为虚函数(除了构造函数之外的非静态函数可以为虚函数)

首先虚函数的解析过程是在运行时而不是编译时.

这个是 copy 网上的答案,我觉得写的很好,比如分两种角度去回答这个问题,vtable 我还不知道,但是从使用角度回答这是挺有功底的。但是要有自己的理解才行。

从存储空间角度 虚函数对应一个vtable,可是这个vtable其实是存储在对象的内存空间的。 那么问题来了,如果构造函数是虚函数,就要通过vtable来调用,可是对象空间还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。

从使用角度 虚函数主要用于在信息不全的情况下,能够使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。 另外,虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数,从而实现多态,也就是实现“一个接口,多种方法”。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此规定构造函数不能是虚函数。

补充:构造函数变成虚函数目的是想构造成不同的对象。可以通过 factory pattern,也就是为每一个要构建的类型再创建一个对应的factory,把问题放到factory的make方法中去解决。这也是C++中的通用解决方案。

析构函数什么时候要定义为虚函数,什么时候不用

当有子类并且子类是含有指针的类,虚析构函数可以析构子类。

当没有子类的时候

static的作用

怎么用一个指向子类的基类指针调用基类的虚函数(强制转换或者指明作用域d->Base::fun())

函数返回值可以是unique_ptr吗,为什么

malloc和new的区别

智能指针enable_shared_from_this

写一个字符串类的移动构造,拷贝构造,赋值构造并模拟这几个过程

final 的作用

  • class XX final {} 声明该类不能被继承

容器中存放不同類型的內容

使用智能指针

vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>(xx, xx));
basket.push_back(make_shared<Bulk_quote>(xx, xx));
basket.back()->net_price();//net_price() 是 Bulk_quote 类才有的方法。

派生类普通指针可以转换成基类指针,派生类的智能指针也可以转换成基类的智能指针

拷贝构造 拷贝赋值op= 析构函数

拷贝构造

如果 class 内部有指针,那么编译器默认的拷贝构造行为是潜拷贝,只是将指针的地址复制了一份,但是还是指向相同的对象。这时候需要我们自己去实现拷贝构造函数,实现深拷贝。

int main() {
  String s1();
  String s2("hello");
  
  String s3(s1); // 拷贝构造
  s3 = s2;  // 拷贝复制
}
class String
{
public:
  String(const char* cstr = 0);
  String(const String& str);
  String& operator=(const String& str);
  ~String();
  char* gt_c_str() const { return m_data; }
};

// 只要有动态分配,就需要析构函数来释放内存
inline String::String(const char* cstr = 0)
{
  if (cstr) {
    m_data = new char[strlen(cstr) + 1];
    strcpy(m_data, cstr);
  } else {
    m_data = new char[1];
    *m_data = '\0';
  }
}

inline String::~String()
{
  delete[] m_data;
}

void main() {
  String s1();
  String s2("hello");
  
  String *p = new String("hello"); // 动态创建对象
  delete p; // 与动态创建对象配套,需要有 delete
}

拷贝赋值

如果使用编译器默认的拷贝复制,那么复制过去的不是对象的内容,而是其他对象的指针。此时 b 就指向 a 的内容,并且 b 之前所指向的内容不再有指针指向它,会造成内存泄漏。

步骤:

1,首先清空左边的内容

2,对左边重新分配和右侧一样大的空间

3,拷贝右侧内容到左侧

inline String& String::operator=(const String& str)
{
  if (this == &this) {
    return *this; // 检测自我赋值,这一步必须要有,如果没有那么在自我赋值的时候,第一步会杀掉自己,那么下一步就无法进行了。
  }
  
  delete[] m_data;
  m_data = new char[strlen(str.m_data) + 1];
  strcpy(m_data, str.m_data);
  return *this;
}

模板类

template<typename T>
class complex {
public:
  complex (T r = 0, T i = 0) : re(r), im(i) {}
  complex& operator += (const complex&);
  T real() const {return re;}
  T imag() const {return im;}
private:
  T re, im;
  friend complex& __doapl(complex*, const complex&);
};

// use
complex<double> c1(2.5, 1.5);

类模板就是将类型抽取出来,等需要用的时候再传进来。这里的 typename 就是将来要传入的类型。

函数模板

template<class T>
inline const T& min(const T& a, const T& b) {
  return b < a ? b : a;
}

class stone {
public:
  stone(int w, int h, int we) : _w(w), _h(h), _weight(we) {}
  bool operator< (const stone& rhs) const {return _weight < rhs.weight;}
private:
  int _w, _h, _weight;
};

stone r1(2, 3), r2(3, 3), r3;
r3 = min(r1, r2);

在调用的时候编译器会对模板方法进行实参推导,推导结果 T 为 stone,于是要调用 stone 内部实现的符号 "<"。这一步有可能会编不过。

成员模板

就是在模板内部还有模板

template <class T1, class T2>
struct pair {
  T1 first;
  T2 second;
  pair() : first(T1()), second(T2()) {}
  pair(const T1& a, const T2& b); first(a), second(b){}
  template <class U1, class U2>
  pair(const pair<U1, U2& p>) : first(p.first), second(p.second) {}
};

最后一个构造函数就是成员模板,但是在传参的时候,U1 必须要满足 first(p.first) 赋值条件,也就是类型必须相同。