C++基础知识复习

第一部分:基础知识

一、const

1. 作用

  • 修饰变量,表示不可能更改

  • 修饰指针

    • const int *ptr——pointer to const
    • int const *ptr—— const pointer
    • 原则:被const修饰的后面的值是不可改变的
  • 修饰引用

    常用于形参。即避免了copy,又避免了对其值的修改

  • 修饰成员函数

    表示该成员函数不能修改成员变量

二、static

1. 作用

  • 修饰普通变量

    修改变量的存储区域,使其存储在静态区。

    在main函数执行前就分配了空间;

    如果没有被显式初始化,则用默认值初始化

  • 修饰普通函数

    表明函数的作用范围。

    仅在定义该函数的文件内才能使用(多人项目中,为了防止与其他namespace重名,可以将函数定义为static)

  • 修饰成员变量

    使其变为类变量,即所有instance公用一个变量

  • 修饰成员函数

    使得不需要构造对象就可以访问此函数

    static函数内不能访问非静态成员(毕竟人家不要构造实例)

三、this指针

  1. this指针是一个隐含于每一个非静态成员函数中的特殊指针,指向调用该成员函数的那个对象。(类似ptyhon类中的self
  2. 当对一个对象调用成员函数时,程序先讲对象的地址赋给this指针,然后调用成员函数;每次成员函数存取数据变量时,都隐式地使用this指针
  3. this指针被隐式地声明为ClassName *const this——意味着不能给this指针赋值;在ClassName类的const成员函数中,this指针的类型为:const ClassName* const,这说明不能修改this指针指向的数据成员。
  4. this并不是一个常规变量,而是一个右值,所以不能取得this的地址(能不能&this
  5. 以下场景中,经常显式的引用this指针:
    • 为实现对象的链式引用
    • 为避免对同一对象进行赋值操作
    • 为实现一些数据结构时,如list

四、inline内连函数

1. 特点

  • 编译时,将内联函数内容在调用处展开
  • 省略了进入函数的步骤,直接执行函数体
  • 类似于,但多了类型检查,具有函数特性
  • 编译器一般不inline包含循环、递归、switch等复杂操作的函数
  • 类中定义的函数,除了虚函数之外,其他函数都会自动隐式地的当成内联函数。

2. 编译器对inline函数的处理步骤

  • inline函数体复制到inline函数调用处
  • 为所有inline函数中的局部变量分配内存空间
  • inline函数的输入参数和返回值映射到调用方法的局部变量中
  • 如果inline函数包括多个返回点,将其转变为inline函数代码块末尾的分支(使用GOTO)

3. 优点

  • 省去了参数压栈、栈帧开辟与回收,结果返回的开销
  • 相比与来讲,在代码展开时,会做安全检查或者自动类型转换
  • 在类中声明同时定义的成员函数,会自动inline,因此内联函数可以访问类的成员变量,但不可以
  • 内联函数在运行时可调试,宏定义不可以

4. 缺点

  • 代码膨胀。若执行函数体的时间比函数调用大很多,则inline收益很小。
  • inline无法随着函数库升级而升级。inline函数的改变需要重新编译,而non-inline可以直接连接
  • 是否内联,程序员不可控。inline函数只是对编译器的建议,是否内联,由编译器决定。

5. 虚函数可以是inline

  • 可以。但当虚函数表现多态时的时候,不能内联
  • 内联发生在编译时,而虚函数的多态性是在运行期,编译器无法知道运行时调用哪个函数。因此虚函数表现为多态时(运行期)不能内联
  • inline virtual唯一可以内联:编译器知道要调用的对象是哪个类(如Base::who()),这只发生在编译器具有实际对象,而非对象的指针或引用。

例子:

#include <iostream>
using namespace std;
class Base {
  public:
    inline virtual void who(){cout << "Base::who"<<endl;}
    virtual ~Base()}{}
}

class Derived : public Base {
  public:
    void who(){cout << "Derived::who"<< endl;}
}

int main(){
  Base b;
  b.who(); // 此处编译其就知道要调用哪个函数
  
  Base *ptr = new Derived();
  ptr->who();// 多态
  
  delete ptr;
  ptr = nullptr;
  return 0;
}

五、sizeof

  • 对数组,返回整个数组占用的空间大小
  • 对指针,返回指针本身占用的空间大小

六、extern "C"

  • extern的函数或变量是extern类型,跨文件访问
  • 被修饰的变量或函数是按照C语言方式编译和链接的
#ifdef __cplusplus
extern "C"{
#endif
  void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif

七、union联合

1. 概念

可以有多个数据成员,但在任意时刻只有一个数据成员可以有值。当某个成员被赋值后,其他成员变成未定义状态。

2. 特点

  • 默认为public
  • 可以有构造函数、析构函数
  • 不能含有引用类型成员
  • 不能继承自其他类,不能作为基类
  • 不能包含虚函数
  • 匿名union在定义所在的作用域可以直接访问union成员
  • 匿名union不能包含protectedprivated成员
  • 全局匿名union必须是静态的
#include <iostream>
using namespace std;

union UnionTest {
  UnionTest():i(10){}; // 构造函数
  int i;
  double d;
};

static union{ // 全局静态匿名union
  int i;
  double d;
};

int main(){
  UnionTest u;
  
  cout << u.i << u.d << endl;
  
  Union {   // 局部匿名union
    int i;
    double d;
  };
  ::i = 20; // 全局静态union.i值
  
  i = 30;// 局部i
    
  return 0;
}

八、C实现C++类

  • 封装

    使用函数指针把属性和方法封装在结构体

  • 继承

    结构体嵌套

  • 多态

    父类与子类方法的函数指针不同

九、explicit

  • 修饰构造函数是,防止隐式转换和复制初始化
  • 修饰转换函数时,防止隐式转换,但按语境转换除外

十、friend友元类和友元函数

  • 能访问私有成员
  • 破坏了封装性
  • 友元函数不可传递
  • 友元函数的单向性
  • 友元声明的形式和数量不受限制

十一、using

1. 引入命名空间的一个成员

using namespace_name::name;

2. C++11中派生类重用基类的构造函数

class Derived : public Base{
  public:
  	using Base::Base; 
};

对于基类的每个构造函数,编译器都会生成一个与之对应(形参类列表完全相同)的派生类构造函数。

3. using指示

using namespace std;// 无需为std里的所有名字添加std前缀了

注:

  1. 应尽量少用using 指示,会污染命名空间
  2. 如果只引入一个成员,且与局部名称冲突了,编译器会发出指示
  3. 但如果全导入了,且覆盖了局部名称,编译器不会提示,排查问题较难

十二、::范围解析符

1. 类别

  • ::name全局作用符。

    用于类型名称(如类、类成员、成员函数、变量)前。

  • class::name类作用域符。

    用于指定类型的作用域范围是具体某个类的。

  • namespace::name命名空间作用域符

    用于指定类型的作用域范围是某个命名空间的。

int count = 1; // ::count

class A{
  public:
  	static int count = 2; // A::count
};
void foo(){
  int count = 3;
}

十三、引用

1. 左值引用

常规引用,表示对象的身份。

2. 右值引用

右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。

右值引用可以实现转移语义(move sementics)和精确传递(perfect forwarding),主要目的有两个:

  • 消除两个对象交互时不必要的对象拷贝,节省存储资源,提升效率
  • 能够更简洁明确地定义泛型函数

3. 引用折叠

  • X& &X& &&X&& &可以折叠为X&
  • X&& &&可以折叠为X&&

十四、成员初始化列表

1. 优点

  • 更高效

    少了一次调用默认构造函数的过程

  • 有些场合必须要初始化列表

    • 常量成员

      因为常量只能初始化,不能赋值,所以必须放在初始化列表中

    • 引用类型

      引用必须在定义时初始化,并不能重新赋值

    • 没有默认构造函数的类类型

      因为使用初始化列表可以不必调用默认构造函数,就可初始化

2. initializer_list

用花括号初始化器初始化一个列表,其中构造函数接受一个std::initializer_list参数

#include <iostream>
#include <vector>
#include <intializer_list>

template<typename T>
struct S{
  std::vector<T> v;
  S(std::initializer_list<T> l): v(l){
    std::cout << "init"<< endl;
  }
  
  void append(std::initializer_list<T> l){
    v.insert(v.end(), l.begin(), l.end());
  }
  
  std::pair<const T*, std::size_t> c_arr() const {
    return {&v[0], v.size()};
  }
};

第二部分:面向对象

一、多态

1. 概念

  • 多态,可以理解为消息以多种形式显示的能力

  • 多态是以封装和继承为基础的

  • C++多态分类和实现

    • 重载多态(Ad-hoc,编译期)

      函数重载,运算符重载

    • 子类型多态(subtype,运行期)

      虚函数

    • 参数多态性(parametric,编译期)

      类模板、函数模板

    • 强制多态(coercion,编译器/运行期)

      基本类型转换、自定义类型转换

2. 静态多态

编译期,早绑定

函数重载

class A{
  void foo();
  void foo(int a);
};

3. 动态多态

运行期/晚绑定

虚函数:virtual修饰

  • 普通函数(非类成员函数)不能是虚函数

  • 静态函数(static)不能是虚函数

  • 构造函数不能是虚函数

    因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成时,才会形成虚表指针(重要)

  • 内联函数不能是表现多态时的虚函数

4. 虚析构函数

是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

Class Base{
  public:
  	Base();
  	virtual ~Base();
};

class Derived : public Base {
  public:
  	...
};

int main(){
  Base *ptr = new Derived();
  delete ptr;
  ptr = nullptr;
  return 0;
}

5. 纯虚函数

一种特殊的虚函数

在基类中不能对虚函数给出有意义的实现,而是声明为纯虚函数,它的实现留给派生类去做。

Class AbstraceBase{
  virtual void foo(int) = 0;
};

虚函数 vs 纯虚函数

  • 类如果声明虚函数,且实现了,哪怕是空实现,则作用就是为了能让这个函数在派生类里被override。这样,编译器就可以使用后期绑定来达到多态了;纯虚函数只是一个接口,是个函数声明而已,要留到子类实现
  • 虚函数在派生类可以不重写;纯虚函数必须在子类实现才可以实例化
  • 虚函数的类用于实作继承,继承接口的同时也继承了父类的实现。纯虚函数关注的是接口的统一性,实现由子类去完成
  • 带纯虚函数的类称为抽象类,不能被实例化。只有继承并实现了纯虚函数,才能使用。派生类继承后,可以继续是抽象类,也可以是普通类。
  • 虚基类是虚继承中的基类。

6. 虚函数指针、虚函数表

  • 虚函数指针

    在含有虚函数类的对象中,指向虚函数表,在运行时确定

  • 虚函数表

    在程序只读数据段.rodata section,存放虚函数指针。如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。

7. 虚继承

用于解决多继承条件下的菱形继承问题

浪费存储空间、存在二义性

底层实现原理与编译器相关,一般通过虚基类指针虚基类表实现。每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用空间)(但虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr指的是虚基类表指针,指向一个虚基类表——记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持公有基类(虚基类)的两份同样拷贝,从而节省了存储空间。

虚继承 vs 虚函数

  • 相同之处

    都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)

  • 不同之处

    • 虚继承
      • 虚基类依旧存在继承类中,之占用存储空间
      • 虚基类表存储的是虚基类的相对直接继承类的偏移
    • 虚函数
      • 虚函数不占用存储空间
      • 虚函数表存储的是虚函数地址

模板类、成员模板、虚函数

  • 模板类可以使用虚函数
  • 一个类的成员模板(本身是模板的成员函数)不能是虚函数

8. 抽象类、接口类、聚合类

  • 抽象类:含纯虚函数
  • 接口类:仅含纯虚函数的抽象类
  • 聚合类:可以直接访问其成员,具有特殊的初始化语法
    • 所有成员都是public
    • 没有定义任何构造函数
    • 没有类内初始化
    • 没有基类、没有virtual函数

第三部分

一、内存分配和管理

1. malloc、calloc、realloc、alloca

  • malloc

    申请指定字节数的内存。申请到的内存中的初始值不确定

  • calloc

    指定长度的对象,分配能容纳指定个数的内存。申请的内存的每一位(bit)都初始化为0

  • realloc

    更改以前分配的内存长度(增加或减少)

    当增加长度是,可能需要将以前分配区的内容移到另一个足够大的区域。而新增的区域内的初始值不确定

  • alloca

    栈上申请内存。

    程序在出栈时,会自动释放内存。但需注意,alloca不具有可移植性,而且在没有传统堆栈的机器上很难实现

    alloca不宜使用在必须广泛移植的程序中。C99中支持变长数组(VLA),可以用来代替alloca。

2. malloc和free

分别用于内存的分配和释放

// 内存申请
char *str = (char*)malloc(100);
assert(str != nullptr);

// 内存释放
free(str);
str = nullptr;

3. new和delete

  • new/new[]
    • 先底层调用malloc分配内存
    • 再调用构造函数(创建对象)
  • delete/delete[]
    • 先调用析构函数(清理资源)
    • 再底层调用free释放空间
  • new申请内存时会自动计算所需要的字节数;malloc则需要我们自己输入申请空间的字节数
int main(){
  T* t = new T(); // 先分配内存,再构造
  delete t; // 先析构,再free
  return 0;
}

4. 定位new

placement new允许我们向new传递额外的地址参数,从而再预先指定的内存区域创建对象。

// place_addr是一个指针
// initializers提供一个以逗号分割的初始值列表
new(place_addr) type;
new(place_addr) type (initializers);
new(place_addr) type [size];
new(place_addr) type [size] {braced intializer list};

5. delete this 合法吗?

合法,但是:

  • 必须保证this对象是通过new(不是new[]、不是placement new、不是栈上、不是全局、不是其他对象成员)分配的
  • 必须保证调用delete this的成员函数是最后一个调用this的成员函数
  • 必须保证成员函数的delete this后面不再调用this
  • 必须保证delete this后没有人再使用了

总之,delete this对调用的成员函数有很严格的要求。

class A{
  public:
  	A(){};
    void destory(){ delete this;} // 必须显式的调用,进行内存空间释放
  private:
   	~A(){}  // 私有函数,只能通过new去动态构造
};

6. 栈上或堆上生成对象

C++中,类对象的建立有两种:

  • 静态建立

    • A a;

    • 由编译器为对象在栈空间分配内存,是通过移动栈顶指针,挪出适当的空间,然后在这片内存空间上调用构造函数,形成一个栈对象。

    • 为什么不把构造函数设置为private?

      构造函数设置为私有,会影响new动态建立对象,因为其第二阶段也会调用构造函数。

  • 只能在

    • 方法:将析构函数设置为私有

      C++是静态绑定语言,编译器管理栈上对象的声明周期。

      编译器为类的对象分配栈空间时。会先检查类的析构函数的访问性;

      若析构函数不可访问,则不能在栈上创建对象。

    • 缺点:无法解决继承问题

      若其作为基类,则析构函数一般要设置为virtual,然后在子类重写,以实现多态,所以析构函数不能设置为private。可以将析构函数设置为protected

class A{
  protected:  // protected,解决继承问题
  	A(){}
  	~A(){}
   public:
  	static A* create(){return new A();} // 静态方法,负责创建
  	void destroy() {delete this;}	// 负责释放内存空间,必须最后调用
};
  • 只能在

    • 方法:将newdelete重载为private

      在堆上生成对象,使用new关键字操作,过程包括两个阶段:

      1. 使用new在堆上需找可用内存,分配给对象
      2. 调用构造函数生成对象

      new操作设置为private,那么第一阶段的操作就无法完成,就不能在堆上生成对象

class A{
  private:
  	void *operator new(size_t){}; // 函数第一个参数和返回值都是固定的
  	void operator delete(void* ptr){};// 重载new就需要重载delete
  public:
  	A(){}
  	~A(){}
};

有个问题:

如果既把析构函数设置为private,也将newdelete重载为了private,那么对象会创建在哪里?创建失败?

二、智能指针

1. 标准库

#include <memory>
std::auto_ptr<std::string> ps (new std::string(str));// C++98
// C++ 11中 auto_ptr被弃用

2. shared_ptr

多个智能指针可以共享同一个对象,对象的最末一个拥有者有责任销毁对象,并清理与该对象相关的所有资源

优点:

  • 支持定制型删除器(custom deleter

  • 可防范Cross-DLL问题

    对象在动态链接库DLL中new创建,却在另一个DLL内被delete销毁

  • 自动解除互斥锁

3. Weak_ptr

允许共享但不拥有某个对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何weak_ptr都会自动成空。

因此,在defaultcopy构造函数之外,weak_ptr只提供接受一个shared_ptr的构造函数

优点:

  • 可打破环状引用

    两个其实已经没有被使用的对象,彼此互指,使之看似还在『被使用』的状态的问题

4. unique_ptr

C++ 11提供的类型,在异常时可以帮助避免资源泄露的智能指针,用于取代auto_ptr

独占式拥有,即一个对象和相应的资源同一时间只被一个pointer拥有。

一旦拥有者被销毁或设置为empty,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,响应资源会被释放。

uique_ptr v.s auto_ptr

  • auto_ptr可以赋值拷贝,复制拷贝后所有权转移;unique_ptr无赋值拷贝语义,但实现了move语义
  • auto_ptr对象不能管理数组(析构调用delete);unique_ptr可以管理数组(析构调用delete[]

5. 强制类型转换

待补充

附录:

posted @ 2021-04-11 12:06  Aurelius84  阅读(319)  评论(0编辑  收藏  举报