学习笔记|C++

本文最后更新于:4 个月前

目录

零、Google C++ 风格指南
  0.文件命名
  1.变量命名
   (1)普通变量、结构体的数据成员
   (2)类的数据成员
  2.常量和枚举值命名
  3.类型命名
  4.函数命名
  5.函数声明与定义
  6.循环、判断、开关
  7.逻辑判断
  8.函数返回值
  9.类格式
  10.初始化列表
  11.模版与转换
一、C++ 对 C 的增强
  1. namespace 命名空间
   (1)定义
   (2)嵌套定义
   (3)命名空间的作用
  2.struct 结构体定义
  3.bool 类型
  4.三目运算符当左值
  5.const
  6.枚举
二、C++ 对 C 的扩展
  1.引用
   (1)引用初始化 与 const
   (2)引用的本质
   (3)指针的引用
   (4)const 修饰引用
   (5)引用作为函数返回值
  2.inline 内联函数
  3.默认参数和占位参数
  4.函数重装
   (1)重载规则
   (2)调用准则
   (3)注意
   (4)底层实现
  5.函数重载和函数指针
三、类和对象
  1.类的基本概念
   (1)概念
   (2)对象的大小
  2.类的封装
   (1)封装的访问属性
   (2)面对对象和面对过程
   (3)以圆的面积为例
   (4)初学者易犯错误模型
   (5)多文件编写
   (6)同类之间可直接访问私有变量
  3.构造函数
   (1)格式
   (2)默认构造函数
  4.析构函数
   (1)格式
   (2)默认析构函数
   (3)析构函数调用顺序
  5.拷贝构造函数
   (1)显式的拷贝构造函数
   (2)默认拷贝构造函数
   (3)拷贝构造函数与赋值操作符函数
   (4)深拷贝与浅拷贝
   (5)匿名对象的拷贝构造
  6.构造函数的初始化列表
   (1)格式
   (2)构造函数调用顺序
   (3)析构函数调用顺序
   (4)const 常量的初始化
  7.new 和 delete
   (1)格式
   (2)异同
   (3)delete 和 delete[] 的区别
  8.static
   (1)静态成员变量
   (2)静态成员函数
  9.this 指针
   (1)问题
   (2)处理机制
   (3)this 指针接收对象地址
   (4)this 指针特点
   (5)设置成员函数只读
   (6)返回对象本身
  10.友元函数与友元类
   (1)友元函数
   (2)友元类
   (3)友元的特性
  11.操作符重载
   (1)类外重载
   (2)类内重载
   (3)操作符重载规则
   (4)实例:重载+=
   (5)实例:重载 ++
   (6)实例:重载 << >>
   (7)重写等号操作符
   (8)实例:重载 ()
   (9)实例:自定义智能指针
  12.综合实战:自定义 string
   (1)临时对象与拷贝省略
   (2)const 的修饰必不可少。
   (3)记得回收原空间。
   (4)判断参数是否特殊。
四、继承
  1.类和类的关系
   (1)has A
   (2)use A
   (3)is A
  2.继承的基本概念
   (1)格式
   (2)定义
   (3)子类的组成
   (4)继承方式
   (5)访问控制权限的判断
  3.继承中的构造和析构
   (1)赋值兼容原则
   (2)继承中构造析构调用原则
   (3)子类父类重名变量和重名函数
   (4)继承中的 static
  4.多继承
  5.虚继承
   (1)多继承中二义性问题
   (2)virtual
五、多态
  1.多态的意义
  2.多态的三个必要条件
  3.虚函数
  4.静态联编和动态联编
  5.虚析构函数
  6.重载、重写、重定义
   (1)重载
   (2)重定义
  7.多态的原理
   (1)虚函数表和 vptr 指针
   (2)验证 VPTR 指针的存在
   (3)VPTR 指针的分步初始化
   (4)父类指针和子类指针步长
   (5)多态的总结
  8.纯虚函数和抽象类
  9.纯虚函数和多继承
六、常见错误
  1.pointer being freed was not allocated

零、Google C++ 风格指南

0.文件命名

全部小写 + 用下划线:my_useful_class.cc

1.变量命名

(1)普通变量、结构体的数据成员

string table_name;  // 全部小写 + 用下划线.

(2)类的数据成员

string table_name_;  // 全部小写 + 后加下划线.

2.常量和枚举值命名

声明为 constexprconst 的变量, 或在程序运行期间其值始终保持不变的, 命名时以 “k” 开头, 大小写混合。

const int kDaysInAWeek = 7;

enum UrlTableErrors {
    kOK = 0,
    kErrorOutOfMemory,
    kErrorMalformedInput,
};

3.类型命名

类、结构体、类型定义 (typedef)、枚举名、类型模板参数:首字母大写 + 没有下划线 + 大小写混合

// 类和结构体
class UrlTable { ...
struct UrlTableProperties { ...

// 类型定义
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// 枚举
enum UrlTableErrors { ...

4.函数命名

常规函数使用首字母大写+ 大小写混合 + 没有下划线, 取值和设值函数则要求与变量名匹配:

// 常规函数
MyExcitingFunction();
MyExcitingMethod();

// 取值函数
my_exciting_member_variable();

// 设值函数
set_my_exciting_member_variable();

5.函数声明与定义

  • 函数名和左圆括号间永远没有空格.
  • 右圆括号和左大括号间总是有一个空格.

函数看上去像这样:

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
  DoSomething();
  ...
}

如果同一行文本太多, 放不下所有参数:

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) {
  DoSomething();
  ...
}

甚至连第一个参数都放不下:

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
    Type par_name1,  // 4 space indent
    Type par_name2,
    Type par_name3) {
  DoSomething();  // 2 space indent
  ...
}

PS:对于单行函数的实现,在大括号内加上空格。

Foo(int b) : Bar(), baz_(b) {}  // 大括号里面是空的话, 不加空格.
void Reset() { baz_ = 0; }  // 用空格把大括号与实现分开.

6.循环、判断、开关

if (condition) {  // 左括号前面、右括号后面 有空格
  ...  // 2 空格缩进.
} else {  // else 与 if 的右括号同一行.
  ...
}

switch (var) {
  case 0: {  // 2 空格缩进   // 冒号后面有空格
    ...      // 4 空格缩进
    break;
  }
  default: { // 冒号后面有空格
    assert(false);
  }
}

while (condition) {   // 左括号前面、右括号后面 有空格
  ...
}

try {        // 空格
  foo();
} 
catch (NSException *ex) {    // 左括号前面、右括号后面 有空格,和 if 一样
  bar(ex);
}

7.逻辑判断

逻辑与 (&&) 操作符总位于行尾。

if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another && last_one) {
  ...
}

8.函数返回值

return result;         // 返回值很简单, 没有圆括号.
// 可以用圆括号把复杂表达式圈起来, 改善可读性.
return (some_long_condition &&
        another_condition);

9.类格式

  • 访问控制块的声明依次序是 public:protected:private: ,每个都缩进 1 个空格,
  • public 外, protected:private: 前要空一行。
  • 继承与初始化列表中的冒号前后恒有空格。
class MyClass : public OtherClass {    // 继承冒号前后各一个空格。
 public:      // 注意有一个空格的缩进
  MyClass();  // 标准的两空格缩进

  void SomeFunction();
  void SomeFunctionThatDoesNothing() {
  }

 private:     // private: 上方要空一行
  bool SomeInternalFunction();

  int some_var_;
  int some_other_var_;
};

10.初始化列表

构造函数初始化列表放在同一行或按四格缩进并排多行。

// 如果所有变量能放在同一行:
MyClass::MyClass(int var) : some_var_(var) {
  DoSomething();
}

// 如果不能放在同一行,
// 必须置于冒号后, 并缩进 4 个空格
MyClass::MyClass(int var)
    : some_var_(var), some_other_var_(var + 1) {
  DoSomething();
}

// 如果初始化列表需要置于多行, 将每一个成员放在单独的一行
// 并逐行对齐
MyClass::MyClass(int var)
    : some_var_(var),             // 4 space indent
      some_other_var_(var + 1) {  // lined up
  DoSomething();
}

// 右大括号 } 可以和左大括号 { 放在同一行
// 如果这样做合适的话
MyClass::MyClass(int var)
    : some_var_(var) {}

11.模版与转换

< 前没有空格,> 和 ( 之间也没有。

vector<string> x;
y = static_cast<char*>(x);

// 在类型与指针操作符之间留空格也可以, 但要保持一致.
vector<char *> x;

一、C++ 对 C 的增强

1. namespace 命名空间

(1)定义

// 定义
namespace spaceA {
  int a = 10;
}

//使用(2种方式)
using namespace spaceA;
using spaceA::a;

(2)嵌套定义

命名空间要引用到最后一层

// 定义
namespace spaceA {
  namespace spaceB {
    int a = 10;
  }
}

// 使用(2种方式)
using namespace spaceA::spaceB;
using spaceA::spaceB::a;

(3)命名空间的作用

可以由用户命名作用域,用来处理程序中常见的同名冲突。

2.struct 结构体定义

struct student {
  string name;
  int age;
};

// C++的定义
student tom;   //少了struct
tom.age = 18;
  
// C语言的定义
struct student tom;

PS:C语言中的 sturct 不可以有成员函数,C++ 中 struct 可以有成员函数。

3.bool 类型

bool flag = truecout << flag;    // 1;   // 只能 1 或 0
cout << sizeof(flag);  // 1个字节,不是 int 的4个字节

if (flag) {
  pass;
}

4.三目运算符当左值

  • 在C中,三目运算符返回的是一个数值,运算器计算出的一个数值,不能当左值;
  • 在C++中,三目运算符返回的是变量的引用,可以直接赋值。
// C语言中,需要返回地址,再改变地址储存的值
*(a > b ? &a : &b) = 10;

// C++中,返回的 a、b 是变量引用,可以直接赋值
(a > b ? a : b) = 10;

5.const

  • const int a = 10; C中,a 是一个变量;C++中,a 是一个常量,储存在常量区的符号表中。
  • C++中 const 类似于 define,但是 define 是在预处理时展开,const 在编译时处理。
const int a = 10;

// C语言中,通过以下方式可以更改 a 的值
int *p = &a;
*p = 20;

// C++中,不可以通过指针(以下方式)修改 a 的值
// int *p = (int*) &a;
// *p = 20;

⚠️辨析:

const * 修饰变量只读,* const 修饰指针只读。

// const *  // 变量 a 的值不能改变
int const *p = &a;
const int *p = &a;

// *const   // 指针 p 的值不能改变
int * const p = &a;

// 对于其他的
const int& b = a;  // 引用 b 的值不能改变。
const int b = a;   // 变量 b 的值不能改变

⚠️注意01:

安全性高的变量,不可以给安全性较低的变量赋值。(同样适用于函数参数传递。)

// 变量赋值的情况
const int a = 10;
int b = a;   // error
// 参数传递的情况
void fun(int b){
  //...
}

const int a = 10;
fun(a);   // error

⚠️注意02:

如果写以下代码,编译会报错:conversion from string literal to 'char *' is deprecated

char *p = "Tom";   // error

const 修饰即可。【原因:“Tom” 为常量,安全性较高,不可以传递给 安全性较低的 char *p

char const *p = "Tom";

6.枚举

enum season {
	spring = 0;
	summer;
	autumn;
	winter;
}

// C语言中,可以用数字代替
enum season s = 1;

// C++中,不可以用数字代替
enum season s = summer;

二、C++ 对 C 的扩展

1.引用

(1)引用初始化 与 const

  • 定义引用时,必须初始化。
int a = 10; 
int &b = a;   // 合法,初始化
int &b;       // 非法,没有初始化
  • non-const引用 只能用 non-const 变量初始化。
int a = 10;    // non-const 变量
const int b = 10;  // const 变量

int &aa = a;   // 合法,用 non-const左值 初始化 non-const引用
int &bb = b;   // 非法,不能用 const 左值 初始化 const引用
  • const 引用 可以用 常量、non-const 变量、const 变量初始化。
int a = 10;    // non-const 变量
const int b = 10;  // const 变量

const int &aa = a;   // 合法
const int &bb = b;   // 合法
const int &cc = 10;  // 合法

(2)引用的本质

  • 引用本质上是一个 常指针。
  • 常指针被存在了常量区,即引用也存储在常量区,(不在栈中,但指向栈中的变量 a )
  • 同理的还有 int arr[10]; ,a 存储在常量区中,指向栈中的10个int空间
int * const p = &a;

(3)指针的引用

struct student* &p 表示 struct student* 类型的引用 &p

struct student {
  int id;
  string name;
}

void free_student(struct student* &p) {  // 指针的引用
  if (p != nullptr) {
    free(p);
    p = nullptr;
  }
}

int main() {
  struct stduent *p = nullptr; 
}

(4)const 修饰引用

对于常量的引用,应该用 const 修饰

const int a = 10;

int fun(const int &a) {
  pass;
}

(5)引用作为函数返回值

返回引用,就是返回 变量本身。

  • 用于返回 函数中的静态变量 或者 类中的静态成员函数返回静态成员变量。
int& fun(){
	static int a = 10;
  return a;
}

int main(){
  cout << fun() << endl;
}
  • 可以当左值。
int& fun(){
	static int a = 10;
  return a;
}

int main(){
  fun() = 20;   // 当左值
}
  • 返回类的对象

this 的使用——返回对象本身

class Test {
private:
  int a;
public:
  Test &Add(Test &another){   // 使用引用 返回类的对象本身。
    this->a += another.a;
    return *this;
  }
};

int main(){
  Test a1(10), a2(20);
  a1.Add().Add();      // 计算 (a1 + a2)+ a1, a1.Add() 返回的是一个对象。
}
  • 禁止:禁止返回局部变量的引用。
int& fun(){
  int a;
  return a;   // error //局部变量 a,离开{},被释放。
}

(6)引用做形参不发生隐式转换

void Fun(double &a){ //... }
  
int main(){
  int a = 10;
  Fun(a);   // 报错!引用做形参不发生隐式转换。若去掉引用,可以隐式转换。
}

2.inline 内联函数

宏函数的缺点:

  • 不对形参进行检查。
  • 对于有表达式的形参,部分不执行,直接替换。
  • 在预处理阶段处理。

inline 函数的优点:

  • 是真正的函数,在编译阶段进行参数检查。

编译器直接将内联函数函数体,插入在函数调用的地方。内联函数,是由编译器在编译阶段处理的

优势:内联函数没有函数调用的额外开销:压栈、跳转、返回

限制:

  • 不能有任何形式的循环语句
  • 不能对函数进行取值操作
  • 不能有过多的条件判断语句
  • 函数体不能过于庞大

本质: 用代码空间换时间

inline void fun(int a) {
  cout << a;
}

3.默认参数和占位参数

默认参数:只能放参数列表最后,没传参时,使用默认参数,传参即使用传递的参数。

int fun(int width, int height = 10) {
  pass;
}

int main() {
  fun(1);
  fun(1, 20);
}

占位参数: 没有意义,用途见 ++ 的重载

int fun(int width, int) {
  pass;
}

int main() {
  fun(1, 20);
}

4.函数重装

(1)重载规则

注意⚠️:返回值不同不是函数重载的构成条件。

  • 函数名相同,形参列表不同:
    • 形参个数
    • 形参类型
    • 形参顺序

(2)调用准则

  • 如果能严格匹配,调用完全匹配的函数
  • 如果不能严格匹配,调用隐式转换
// fun01
int fun(double a) {
  pass;
}

// fun02
int fun(int a) {
  pass;
}

// fun03
int fun(char a) {
 pass; 
}

int main() {
  fun(1.2f)  // 调用 fun01,因为 float 隐式转换为 double。
  fun("a")   // 调用 fun03,若没有 fun03,则 char 隐式转换为 int,调用 fun02。
}

(3)注意

  • 函数重载,写默认参数,警惕函数冲突
int fun(int a, int b) {
  pass;
}

int fun(int a, int b, int c = 10) {
  pass;
}

int main() {
  fun(1, 2);   // 此时产生歧义
}

(4)底层实现

  • C++ 利用 name mangling 来更改函数名,区分参数不同的同名函数
  • 用 v c i f l d 表示 void char int float long double 及其引用
// example
void fun(int a);         // fun_i (int a)
void fun(char c, int a)  // fun_ci (char c, int a)

5.函数重载和函数指针

函数指针一旦定了,就只会指向一个函数类型,不再会发生函数重载。

函数指针 p 指向的函数类型是 int fun(int, int)p(1, 2, 3); 不会发生函数重载,会报错。

  • 函数指针赋值时,会发生函数重载:p = fun;

  • 函数指针调用时,不会发生函数重载,会严格匹配。因为赋值时,已决定 p 指向的函数类型。

int fun(int a, int b) {
  pass;
}

int fun(int a, int b, int c) {
  pass;
}

int (*p)(int, int) = NULL;

// 赋值,发生函数重载
p = fun;

// 调用,不发生函数重载
p(1, 2);    // correct
p(1, 2, 3); // error,不能函数重载
p(a, b);    // error, 不能隐式转换

三、类和对象

类:数据类型

对象:内存中的一块空间

数据类型:这块内存空间,赋予它的类型

变量:内存空间的名称

值:内存空间的内容

1.类的基本概念

(1)概念

类就是一个结构体,区别在于:

  • 类可以设置访问控制权限(默认是 private),结构体默认访问控制权限是 public。
  • 类中可以有成员函数,C++ 结构体也可以有成员函数,但 C 语言结构体只能有成员变量。
class Animal {  // class {}内 叫类的内部,{}外 叫类的外部。
public:        // public 下的成员变量的和成员函数,类内部和外部都可以访问。
    char name[64];
    int fake_age;
    void SayAge(){
        cout << name << ": " << fake_age << endl;
    }
  
private:      // private 下的成员变量和成员函数,只能在类的内部访问。
    int true_age;
};   // 【重要】有分号!


int main() {
    Animal dog;
    // dog.name 是字符串数组,不能复制,不可以直接 dog.name = "xiaohuang";
    memcpy(dog.name, "xiaohuang", sizeof("xiaohuang"));
    dog.fake_age = 2;
    dog.SayAge();
}

(2)对象的大小

  • 对象的大小和结构体的大小一样,按照成员变量内存对齐计算大小。
  • 函数储存在代码段中,不计算在对象的大小中。
  • 静态成员变量,在 data 区,不计算在对象的大小中。

2.类的封装

封装可以到达对内开放数据,对外屏蔽数据,只提供接口。

(1)封装的访问属性

访问属性属性访问权限
pubilc公有类内外均可访问
protected保护类内访问
private(默认)私有类内访问
class Data {
  int year;      // 默认为 private  
};

(2)面对对象和面对过程

面向过程:吃(人, 饭)

面对对象:人.吃(饭)

(3)以圆的面积为例

面对过程:

double CircleGrith(double r){
    return 2 * 3.14 * r;
}
double CircleArea(double r){
    return 3.14 * r * r;
}

面向对象:

class Circle {
public:
  void SetR(int new_r){
    m_r = new_r;
  }
  double CircleGirth(){
    return 2 * 3.14 * m_r;
  }
  double CircleArea(){
    return 3.14 * m_r * m_r;
  }

private:
  double m_r;
};

(4)初学者易犯错误模型

对于成员的定义,一定不要使用表达式。因为在定义 area 时,r 是一个内存空间中的随机值。

class circle {
public:
  double r;
  double area = 3.14 * r * r;   // error,此时 r 是随机值。
};

(5)多文件编写

如果文件链接失败,可以将 circle.cc 改为 circle.hpp ,并在 main.cc 中调用 #include "circle.hpp"

circle.h

#pragma once

class Circle {
public:
    void set_r(double r);
    double CircleGrith();
    double CircleArea();

private:
    double m_r;
};

circle.cc

#include "circle.h"

void Circle::set_r(double r){
    m_r = r;
}

double Circle::CircleGrith(){
    return 2 * 3.14 * m_r;
}

double Circle::CircleArea(){
    return 3.14 * m_r * m_r;
}

main.cc

#include <iostream>
#include "circle.h"

using namespace std;

int main(){
    Circle c;
    int r;
    cin >> r;
    c.set_r(r);
    cout << c.CircleGrith() << endl;
    cout << c.CircleArea() << endl;
}

(6)同类之间可直接访问私有变量

在类的定义中,成员函数需要调用 同类实例化的对象的 私有成员变量,可以直接访问,无需使用 set、get。

原因:相同的类,私有成员变量都是一样的,无需隐藏。

class Circle {
public:
    void SetR(int new_r){
    	m_r = new_r;
  	}
	  void judgeCircle(Circle &another_circle){
      if (m_r == another_circle.m_r /* 【同类之间,可以直接访问私有变量】 */) {
        cout << "两个圆相同!" << endl;
      }
    }

private:
    double m_r;
};

3.构造函数

实例化时,定义成员变量,却不赋值,不安全!所以需要在实例化对象时,对成员变量进行赋值。

(1)格式

构造函数没有返回值、函数类型,和类名同名,构造函数可以重载

class Test {
public:
  Test(){ // 无参构造函数 
    m_x = 0;
    m_y = 0;
  }
  Test(int x, int y){  // 全参构造函数
    m_x = x;
    m_y = y;
  }
  Test(int x){
    m_x = x;
    m_y = 0;
  }

private:
  int m_x;
  int m_y;
};

int main(){
  Test t1;          // 调用无参构造函数
  Test t2(10);   
  Test t3(10, 20);   // 调用全参构造函数
}

(2)默认构造函数

默认构造函数是空函数。

  • 当没有任何构造函数时,会有默认构造函数。
  • 一旦有任何显式构造函数(无参或有参),默认构造函数就会消失。

4.析构函数

析构函数在对象内存释放之前,自动调用。用来释放堆区空间(该对象开辟的)。

(1)格式

class Test {
public:
  ~Test(){ // 析构函数
    if (p != nullptr) {
      free(p);
      p = nullptr;
    }
  }

private:
  int *p;
}

2020-05-25-bjnOV8

(2)默认析构函数

默认构造函数是空函数。

  • 当没有任何析构函数时,会有默认析构函数。
  • 一旦有任何显式析构函数,默认析构函数就会消失。

(3)析构函数调用顺序

析构函数调用顺序,与构造函数相反,【先构造的对象,后析构】。

原因:压入栈的顺序,先构造的对象先压栈,后弹栈。

5.拷贝构造函数

类 Test 实例化出 对象 t1、对象 t2。用 t1 给 t2 赋值。

Test t1;
Test t2(t1);

// 或者写成
Test t2 = t1;

(1)显式的拷贝构造函数

显式的拷贝构造函数,用户自定义的。

Test(const Test &another_test){  // 用 const 来修饰传入的对象。
  m_x = another_test.m_x;
}

(2)默认拷贝构造函数

  • 如果没显式拷贝构造函数,会有一个默认的拷贝构造函数。

  • 将一个对象的成员变量的值,全部拷贝给另外一个对象的成员变量。

(3)拷贝构造函数与赋值操作符函数

此处调用的不是拷贝构造函数,因为 构造函数是对象初始化的时候调用

PS:类中存在默认的赋值操作符函数。

Test t1;
Test t2;
t2 = t1;   // 调用的不是拷贝构造函数,而是【赋值操作符函数】

(4)深拷贝与浅拷贝

浅拷贝的问题:用一个对象给另一个对象赋值,若存在指向堆区的指针,浅拷贝会只拷贝指针所储存的值(堆区地址),析构函数将对此堆区同一空间进行二次释放。

// 浅拷贝:默认拷贝函数
class Teacher {
public:
    // 有参构造
    Teacher(char const *name){              // 【⚠️重要】一定要加 const 修饰!见 一.5.注意02
        int len = strlen(name);
        m_name = (char *)malloc(len + 1);
        strcpy(m_name, name);
    }
    ~Teacher(){
        if (m_name != NULL) {
            free(m_name);
            m_name = nullptr;
        }
    }
    void Print(){
        cout << m_name << endl;
    }

private:
    char *m_name;
};

void Test(Teacher t2){   // 此处调用 t2 的默认拷贝函数,对值进行拷贝
    t2.Print();
    return;
}

int main(){
    Teacher t1("tom");
    Test(t1);
    return 0;
}

深拷贝:提供显式的拷贝构造函数,开辟空间,拷贝指针所指向的空间。

// 深拷贝
Teacher(const Teacher &another_teacher){  // 【⚠️重要】一定要加 const 修饰!见 三.12.(2)
  int len = strlen(another_teacher.m_name);
  m_name = (char *)malloc(len + 1);
  strcpy(m_name, another_teacher.m_name);
}

(5)匿名对象的拷贝构造

匿名对象不能返回引用。匿名对象使用完,内存回收,返回引用没有意义。

返回对象—对象做拷贝

返回对象引用—对象本身

当函数返回值是 对象 时,函数将 该对象拷贝给一个匿名对象。

Test fun(){
	Test temp(10, 20);
	return temp;     // 匿名对象 = tmp,此处调用了匿名对象的拷贝构造函数。
}
  1. 没有变量来接收

函数外部没有变量来接收函数返回值–匿名对象时,匿名对象将不会再被使用,将等待程序结束被回收。

int main(){
	fun();
  return 0;
}
  1. 新实例化一个对象来接收

此处将不再使用拷贝构造函数,匿名对象直接转正为 t,将匿名对象取名为 t;

int main(){
	Test t = fun();	  // 不使用拷贝构造函数。
  return 0;
}
  1. 已有对象来接收

t 已经分配空间,所以匿名对象不在转正。

  • 调用 t 的赋值操作符函数。
  • 系统 【立刻回收】 匿名对象内存。
int main(){
  Test t;
  t = fun();    // 调用 t 的赋值操作符函数。
}

6.构造函数的初始化列表

当一个类 B 的成员变量是另一个类 A 时, 且类 A 的只有带参的构造函数。若要用类 B 实例化对象,必须在类 B 的构造函数调用的之前,调用类 A 的构造函数。

原因:在初始化类 B 的对象 b 时,必须先初始化类 A 的对象 m_a1、m_a2,不然无法给类 B 的对象 b 分配空间。

(1)格式

class A {
public:
    A(int a){
        m_a = a;
    }

private:
    int m_a;
};

class B {
public:
    B(A &a1, A &a2, int b):m_a1(a1), m_a2(a2){  // 构造函数的初始化列表,使用【拷贝构造函数】
        m_b = b;
       // m_a1= a1;  //【错误】这样调用的就不是构造函数,而是赋值操作符函数。
    }
    // 类 B 构造函数重载
  	B(int a1, int a2, int b):m_a1(al), m_a2(a2){  // 构造函数的初始化列表,使用【构造函数】
      	m_b = b;
    }

private:
    int m_b;
    A m_a1;
    A m_a2;
};

int main(){
    A a1(100), a2(200);
    B b1(a1, a2, 300);
  	B b2(100, 200, 300);
    return 0;
}

(2)构造函数调用顺序

调用 对象成员 的构造函数 的顺序,和初始化列表的顺序无关,和定义顺序有关。

以对象 b1 的初始化为例:

  1. m_a1(a1) :因为 m_a1 先定义。A m_a1;

  2. m_a2(a2) :因为 m_a2 后定义。A m_a2;

  3. B() :因为要先调用 m_a1、m_a2 的拷贝构造函数,才能调用 b1 的构造函数。

(3)析构函数调用顺序

以对象 b1 为例:

  1. ~B() :后构造,先析构。
  2. ~A() :m_a2 的析构函数。
  3. ~A() :m_a1 的析后函数。

(4)const 常量的初始化

const 常量不允许在构造函数中赋值,应该定义时边初始化。故在参数列表中进行初始化。

class A {
public:
    A(int a1, int a2, int k) : k_a(k) /* const 常量初始化。*/ {  
        m_a1 = a1;
        m_a2 = a2;
    }

private:
    int m_a1;
    int m_a2;
    const int k_a;   // 定义 const 常量
};

class B {
public:
    B(int k, int b, int a1, int a2, int k_a) : m_a(a1, a2, k_a), k_b(k) /* const 常量初始化。*/{
        m_b = b;
    }

private:
    int m_b;
    A m_a;
    const int k_b;   // 定义 const 常量
};

int main(){
    B b(1, 2, 3, 4, 5);
    return 0;
}

7.new 和 delete

new 和 delete 是操作符,不是函数,因此执行效率高。用来替代C语言的库函数 malloc 和 free。

(1)格式

// 申请一个 int 大小空间。
int *p = new int;      
// int *p = new int(10);  // 表示申请一个 int 大小空间,并初始化为 10
*p = 10;
if (p != nullptr) {
  delete p;
  p = nullptr;
}

// 申请一个数组空间。
int *array_p = new int[10];  // 注意:是中括号,不是小括号,小括号是初始化。
if (array_p != nullptr) {
  delete[] array_p;        // 注意:有个[]
}

(2)异同

【相同的地方】:

new / detele 和 malloc / free 的 内存管理方式是互相兼容的。new 出的空间,可以用 free 释放。malloc 出的空间可以用 dlelte 释放。

【不同的地方】:

  • 执行效率

    new / delete 是操作符,malloc / free 是 C库函数。操作符效率更高,因为不需要压栈弹栈。

  • 调用类函数

    new 可以调用构造函数或拷贝构造函数;malloc 不能调用。

    delete 可以在释放空间前调用析构函数;free 不能调用。

class Test {
public:
    Test(int a, int b){
        m_a = a;
        m_b = b;
    }

private:
    int m_a;
    int m_b;
};

int main(){
    Test *p = new Test(10, 20);
    // Test *p = (Test *)malloc(sizeof(Test));  // error, 
}

(3)delete 和 delete[] 的区别

new 先申请空间,再调用构造函数;delete 先调用析构函数,再释放空间。

Test *p = new Test[10];    // 调用了 10 次构造函数

delete[] p;      // 调用了 10 次析构函数

delete p;        // 只调用一次析构函数,容易造成内存泄露。

8.static

静态局部变量:

  • 储存在 Data 区,默认初始化为 0。
  • 程序结束才自动释放,只初始化一次。
  • 编译阶段就已分配空间,所以不能用变量初始化。

静态全局变量:

  • 只能在本文件中使用,不可多文件使用。
class Test {
public:
  static int& get_a(){
    return m_a;
  }

public:
  static int m_b;    // 声明
private:
  static int m_a;    // 声明
};

int Test::a = 0;   // 初始化
int Test::b = 20;  // 初始化

int main(){
  cout << Test::get_a();   // 调用,取值,函数的命名空间属于类。
  Test::get_a() = 20;      // 调用,赋值,
  
  // 直接通过类的取值
  cout << Test::a << endl;   // java 中是直接 类.静态变量 访问
}

(1)静态成员变量

  • 储存在 Data 区,不在对象的内存当中,不占用类打大小。
  • 程序结束才自动释放,只初始化一次,多个对象共享一个静态成员变量。
  • 编译阶段就已分配空间。初始化在类外初始化。

(2)静态成员函数

静态成员函数只能访问静态数据成员。

原因:非静态成员函数,在调用时 this 指针被当作参数传进。而静态成员函数属于类,而不属于对象,没有 this 指针。

9.this 指针

(1)问题

a1.get_a(); a2.get_a(); 调用的是同一段代码,返回的值不一样?

// 类 Test 的成员函数
int Test::get_a(){
  return m_a;
}

a1.get_a();
a2.get_a();

(2)处理机制

调用函数时,参数中 传递了对象的地址

PS:成员变量以结构体类型储存,成员函数以及静态变量不在对象内存中。见 对象的大小

2020-05-25-aM9eK7

(3)this 指针接收对象地址

// 实际代码是这样的:
int Test::get_a(Test *this){   // 用指针 this 来接收对象 a1 的地址。
  return this->m_a;
}

a1.get_a(&a1);   // 传递对象 a1 的地址。

(4)this 指针特点

This 是常指针,Test *const this

int get(){
	this->m_a = 10; // 正确!可以修改指针所指向内存的值。
  this++;        // 错误! 不可以修改指针所储存的值。
  return this->m_a;
}

(5)设置成员函数只读

格式:在函数后加入 const。

int get() const{   // 加 const 修饰 this 指针所指内容只读。
  return this->m_a;
}

含义:加 const 修饰 this 指针所指内容只读(加的是第一个 const, 修饰 *,第二个 const 原本就有)。

int get(const Test * const this);

(6)返回对象本身

  • 返回值用 引用类型
  • *this 表示对象本身。

用途:对于一个对象,连续调用其成员方法。

class Test {
public:
  Test &Add(Test &another){   // 使用引用 返回类的对象本身。
    this->a += another.a;
    return *this;
  }

private:
  int a;
};

int main(){
  Test a1(10), a2(20);
  a1.Add(a2).Add(a1);      // 计算 (a1 + a2)+ a1 
                           // a1.Add() 返回的是对象 a1。
}

10.友元函数与友元类

(1)友元函数

类外的友元函数可以直接访问类的私有成员变量。

弊端:破坏了类的封装性和隐蔽性。(类型 goto ,不推荐使用。)

class Test {
public:
  friend void fun(Test &t);  // 友元函数的定义。

private:
  int x;
  int y;
};

void fun(Test &t){
  // 频繁的压栈出栈影响效率。
  // cout << t.get_x() * t.get_x() + t.get_y() * t.get_y();
  // 使用友元函数直接访问私有成员,提高效率。
	cout << t.x * t.x + t.y * t.y << endl;
}

声明其他类中的成员方法为友元:(不推荐使用)

class Test;    // 保证第5行中,形参定义不报错。

class A {
public:
  int ReturnTest(Test &t);   // 保证第13行中,友元声明不报错。
}

class Test {
public:
  friend int A::ReturnTest(Test &t);  // 其他类中的成员方法为友元。

private:
  int x;
  int y;
};

int A::ReturnTest(Test &t){    // 先定义后实现,是因为,使用私有成员要在声明私有成员之后。
	return t.x + t.y;
}

(2)友元类

在 A 类中声明 B 类为友元类,B 类中可以直接访问 A 类中的私有成员。

class A {
public:
  friend class B;

private:
	int a;
};

class B {
public:
	void Print(){
		cout << obj_a.a << endl;
  }

private:
  A obj_a;
};

(3)友元的特性

  1. 声明位置

    friend 友元声明写在类定义中,不受其所在类的声明区域 public、private 和 protected 的影响。

  2. 友元关系不能被继承

  3. 友元关系是单向的,不具有交换性

    若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有相应的声明。

  4. 友元关系不具有传递性

    若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一 定 是类 A 的友元,同样要看类中是否有相应的声明。

11.操作符重载

运算符重载的本质是函数重载。

运算符重载的例子:int + intdouble + double。计算机对整数、单精度数和双精度数的 加法操作过程是很不相同的, 但由于C++已经对运算符”+”进行了重载,所以就 能适用于int, float, doUble类型的运算。

(1)类外重载

全局函数

class Test {
public:
  Test(int num){
    this->num = num;
  }
  friend Test operator+(const Test &a, const Test &b);  // 以便 operator+ 函数可以直接调用私有成员变量。

private:
  int num;
};

// 类外定义 + 号重载。
Test operator+(const Test &a, const Test &b){ // 返回值不能为引用,匿名对象返回引用,匿名对象会被回收内存。
  Test tmp(a.num + b.num);
  return tmp;
}

int main(){
  Test a(1), b(2);
  Test c = a + b;                 // 调用 + 号重载 的方式一
  Test d = operator+(a, b);       // 调用 + 号重载 的方式二,传递两个参数
}

(2)类内重载

成员函数

class Test {
public:
  Test(int num){
    this->num = num;
  }
  // 类内定义 + 号重载。
	Test operator+(const Test &another){   // 返回值不能为引用,匿名对象返回引用,匿名对象会被回收内存。
  	Test tmp(this->num + another.num);
    return tmp;
	}

private:
  int num;
};

int main(){
  Test a(1), b(2);
  Test c = a + b;                 // 调用 + 号重载 的方式一
  Test d = a.operator+(b);       // 调用 + 号重载 的方式二,传递一个参数
}

(3)操作符重载规则

  1. 只能对 C++ 允许重载的操作符进行重载。
2020-05-26-higeP3
  1. 重载不能改变运算符的运算对象(即操作数)的个数。

    关系运算符 >< 等是双目运算符,重载后仍为双目运算符,需要两个参数。运算符 +-*& 等既可以作为单目运算符,也可以作为双目运算符,可以分别将它们重载为单目运算符或双目运算符。

  2. 重载不能改变运算符的优先级别。

    */ 优先级高于 +- ,不论怎样进行重载,各运算符之间 的优先级不会改变。

  3. 重载不能改变运算符的结合性。

    = 是从右向左结核性,重载后也是从右向左。

  4. 重载运算符的函数不能有默认的参数。

  5. 重载的运算符必须和用户定义的自定义类型的对象一起使用。

  6. 用于类对象的运算符一般必须重载,但有两个例外,运算符”=“和运算 符”&“不 必用户重载。

    赋值运算符 = 可以用于每一个类对象,可以用它在同类对象之间相互赋值。 因为系统已为每一个新声明的类重载了一个赋值运算符,它的作用是逐个复制类中的数据成员地址运算符 & 也不必重载,它能返回类对象在内存中的起始地址。

  7. 应当使重载运算符的功能类似于该运算符原含义。

(4)实例:重载+=

重载双目运算符。

  • 类外重载
class Test {
public:
    Test(int x){
        this->x = x;
    }
    void Print(){
        cout << x << endl;
    }
    friend Test &operator+=(Test &t1, Test &t2);

private:
    int x;
};

// += 类外重载
Test &operator+=(Test &t1, Test &t2){   // 双目运算符,要返回对象t1本身,所以要用引用。
    t1.x += t2.x;
    return t1;       // 返回对象t1,实际上返回对象的引用。
}

int main(){
    Test t1(1), t2(2);
    t1 += t2;    // operator+=(t1, t2);
    t1.Print();   // 打印结果为 3
}
  • 类内重载
class Test {
public:
    Test(int x){
        this->x = x;
    }
    void Print(){
        cout << x << endl;
    }
    // += 类外重载
    Test &operator+=(const Test &another){  // 双目运算符,要返回对象t1本身,所以要用引用。
        this->x += another.x;
        return *this;    // 返回对象t1,实际上返回对象的引用。
    }

private:
    int x;
};

int main(){
    Test t1(1), t2(2);
    t1 += t2;
    t1.Print();   // 打印结果为 3
}

(5)实例:重载 ++

重载单目运算符

单目运算符 ++ 的重载要写两个,因为 前置++后置++ 不一样。

区别在于:

  • 前置++,++a,返回对象本身,可以连加。
  • 后置++,a++,返回匿名对象,不可以连加。

【 用参数占位符区分 前置++ 和 后置++】 占位参数

class Test {
public:
    Test(int x){
        this->x = x;
    }
    void Print(){
        cout << x << endl;
    }
    friend Test &operator++(Test &t);
    friend const Test operator++(Test &t, int);

private:
    int x;
};

// 重载前置++,比如 t++
Test &operator++(Test &t){   // 返回对象t本身,使用引用
    t.x ++;
    return t;
}

// 重载后置++,比如 ++t
// 用参数占位符区分 前置++ 和 后置++
const Test operator++(Test &t, int){    // 返回一个匿名对象,const = 不可以修改、不可以连加
    Test tmp(t);   
    t.x ++;
    return tmp;    // 返回 ++ 前的值。
}

int main(){
    Test t1(1), t2(2);
    t1++;
    ++++t2;
    t1.Print();
    t2.Print();
    return 0;
}

(6)实例:重载 << >>

作用:直接打印出对象的成员变量。

注意:重载不能写在类的内部,因为会 改变参数顺序

比如:类 Complex 有 x、y 两个坐标值。通过 << >> 实现设值和打印。

  • 类外重载

cout 的类型:ostream。

cin 的类型:istream。

class Complex {
public:
    Complex(int x, int y){
        this->x = x;
        this->y = y;
    }
    friend ostream &operator<<(ostream &os, Complex &c);
    friend istream &operator>>(istream &is, Complex &c);

private:
    int x;
    int y;
};

// 左移重载
// ostream 是 cout 的类型
// 需要 连续使用左移运算符,如: cout << x << y;  // 所以返回 cout 对象本身
ostream &operator<<(ostream &os, Complex &c){
        os << "(" << c.x << "," << c.y << ")" << endl;
        return os;
    }

// 右移重载
// istream 是 cin 的类型
// 需要 连续使用右移运算符,如: cin >> x >> y;  // 所以返回 cin 对象本身
istream &operator>>(istream &is, Complex &c){
    cout << "x:";
    is >> c.x;
    cout << "y:";
    is >> c.y;
    return is;
}

int main(){
    Complex a(1, 1);
    cin >> a;
    cout << a;
}
  • 类内重载

不允许,改变了参数顺序。

ostream &operator<<(ostream &os){} 第一个参数是对象本身,第二参数是cout,所以调用时不能写成 cout << a;,而是 a << cout;

class Complex {
public:
    Complex(int x, int y){
        this->x = x;
        this->y = y;
    }
    ostream &operator<<(ostream &os){   // 第一个参数是对象,第二个参数是cout
        os << "(" << this->x << "," << this->y << ")" << endl;
        return os;
    }

private:
    int x;
    int y;
};

int main(){
    Complex a(1, 1);
		a << cout;             // 调用的第一种方式,改变了操作符实际顺序,禁止!
		a.operator(cout);      // 调用的第二种方式
}

(7)重写等号操作符

  • 为什么要重写等号操作符?

    类中有默认等号操作符函数,函数内容和默认拷贝构造的函数类似,均是浅拷贝,若类中使用了堆区空间,就需要重写等号操作符函数。

class Test {
public:
    // 无参构造函数
    Test(){
        this->id = 0;
        this->name = nullptr;
    }
    // 全参构造函数
    Test(int id, const char *name){
        this->id = id;
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
    }
    // 拷贝构造函数
    Test(Test &another){
        this->id = another.id;
        this->name = new char[strlen(another.name) + 1];
        strcpy(this->name, another.name);
    }
    // 重写等号操作符函数
    Test &operator=(Test &another){
        // 防止自身辅助
        if(this == &another){    // 【注意】判断地址是否相同!& 是去地址。
            return *this;
        }

        // 先回收原本的空间
        if (this->name != nullptr) {
            delete[] this->name;
        }

        // 申请堆区空间赋值
        this->id = another.id;
        this->name = new char[strlen(another.name) + 1];
        strcpy(this->name, another.name);
        return *this;
    }
    // 析构函数
    ~Test(){
        if (this->name != nullptr) {
            delete[] this->name;
            this->name = nullptr;
        }
    }

private:
    int id;
    char *name;
};

(8)实例:重载 ()

伪函数、仿函数、函数对象:将一个对象 当场普通函数来调用。STL 中较多。

class Test {
public:
    int operator()(int value){   // 小括号重载,从而实现伪函数。
        return value * value;
    }
  	int operator()(int value1, int value2){   // 小括号重载,参数个数不同。
        return value1 * value2;
    }
};

int main(){
    Test t;
  	int res = t(2);  // 【伪函数、仿函数】将一个对象 当场普通函数来调用。
    // int res = t.operator()(2);
  	int res = t(2, 3);
    return 0;
}

(9)不建议重载 ||&&

  • ||&& 有短路现象
int a = 0;
if (a && a = 10) {
  pass;
}
cout << a;  // 此时 a 为 0; // 因为 && 有短路现象,第一个数为零,后面的参数直接忽略。

int b = 1;
if (b || b = 10) {
  pass;
}
cout << b;  // 此时 b 为 1;// 因为 || 有短路现象,第一个数为非零,后面的参数直接忽略。
  • 重载后的 ||&& 没有短路现象
class Test {
public:
	bool operator&&(Test &another){
    return this->x && another.x;
  }
  Test operator+(Tes &another){
    Test tmp(this->x + another.x);
    return tmp;
  }

private:
	int x;
};

int main(){
  Test t1(0), t2(1), t3(2);
  // 相当于 t1.operator&&(t2.operator+(t3))
  // 先执行 t2.operator+(t3),再执行 t1.operator&&(),所以没有短路现象。
  if (t1 && (t2 + t3)) {  
    //
  }
}

(9)实例:自定义智能指针

智能指针自动回收申请的堆区空间。

  • 智能指针
#include <memory>

int main(){
  shared_ptr<int> p(new int);   // 智能指针。
  *p = 10;
}
  • 自定义智能指针

定义智能指针类 MyAutoPtr + 重载 -> 操作符函数 + 重载 * 操作符函数

class A {
public:
    A(int a){
        this->a = a;
        cout << "构造函数" << endl;
    }
    void Print(){
        cout << "a = " << this->a << endl;
    }
    ~A(){
        cout << "析构函数" << endl;        
    }

private:
    int a;
};

class MyAutoPtr {
public:
    MyAutoPtr(A *p){
        this->p = p;
    }
    ~MyAutoPtr(){
        if (this->p != nullptr) {
            delete p;
            p = nullptr;
        }       
    }
    A *operator->(){      // 重载 -> 
        return this->p;      // 返回 ptr 私有成员 p 指针
    }
    A &operator*(){       // 重载 *
        return *(this->p);    // 返回 p 所指向的对象本身。
    }

private:
    A *p;
};

void Fun(){
    MyAutoPtr ptr(new A(10));
    // 调用 -> 操作符函数
    ptr->Print();    // ptr.operator->()->Print();
    // 调用 * 操作符函数。
    (*ptr).Print();  // ptr.operator*().Print();
}

int main(){
    Fun();
    return 0;
}

⚠️注意:

重载 -> 操作符函数ptr->Print() 变成了 (ptr.p)->Print() ,返回值为 p,重载后 -> 保留。

重载 * 操作符函数*ptr 变成了 *(ptr.p)返回值却为 *p ,重载后* 不保留。(如果 * 保留的话,应该返回值为 p

12.综合实战:自定义 string

class MyString {
public:
    // 无参构造
    MyString(){
        len = 0;
        str = nullptr;
        cout << "无参构造" << endl;
    }
    // 有参构造
    MyString(const char *str){
        if (str == nullptr) {
            this->len = 0;
            this->str = new char[1];
            strcpy(this->str, "");
        } else {
            this->len = strlen(str);
            this->str = new char[this->len + 1];
            strcpy(this->str, str);
            cout << "有参构造" << endl;
        }
    }
    // 拷贝构造
    MyString(const MyString &another){
        this->len = another.len;
        this->str = new char[this->len + 1];
        strcpy(this->str, another.str);
        cout << "拷贝构造" << endl;
    }
    // 重载 =
    MyString &operator=(const MyString &another){
        // 防止自身赋值
        if (this == &another) {
            return *this;
        }
        // 回收原空间
        if (this->str != nullptr) {
            delete[] this->str;
            this->str = nullptr;
            this->len = 0;
        }
        // 赋值
        this->len = another.len;
        this->str = new char[this->len + 1];
        strcpy(this->str, another.str);
        cout << "重载 = _对象" << endl;
        return *this;
    }
    // 重载 =
    MyString &operator=(const char *p){
      	// 回收原空间
        if (this->str != nullptr) {
            delete[] this->str;
            this->str = nullptr;
            this->len = 0;
        }
        // 赋值
        this->len = strlen(p);
        this->str = new char[this->len + 1];
        strcpy(this->str, p);
        cout << "重载 = _字符 " << endl;
        return *this;
    }
    // 析构函数
    ~MyString(){
        if (this->str != nullptr) {
            delete[] this->str;
            this->str = nullptr;
            this->len = 0;
        }
        cout << "析构函数" << endl;
    }
    // 重载 []
    char &operator[](int index){
        return this->str[index];
    }
    // 重载 +
    MyString operator+(const MyString &another){
        MyString tmp;
        tmp.len = this->len + another.len;
        tmp.str = new char[tmp.len + 1];
        strcpy(tmp.str, this->str);
        strcat(tmp.str, another.str);
        cout << "重载 + " << endl;
        return tmp;
    }
    // 重载 ==
    bool operator==(const MyString &another){
        cout << "重载 == " << endl;
        if (strcmp(this->str, another.str)) {
            return false;
        } else {
            return true;
        }
    }
    // 重载 !=
    bool operator!=(const MyString &another){
        cout << "重载 != " << endl;
        if (strcmp(this->str, another.str)) {
            return true;
        } else {
            return false;
        }
    }
    //重载 <<
    friend ostream &operator<<(ostream &os, MyString &s);
    friend istream &operator>>(istream &is, MyString &s);

private:
    int len;
    char *str;
};

//重载 <<
ostream &operator<<(ostream &os, const MyString &s){
    for (int i = 0; i < s.len; i++) {
        os << s[i];
    }
    cout << "重载 << " << endl;
    return os;
}
//重载 >>
istream &operator>>(istream &is, MyString &s){
    // 回收原空间
        if (s.str != nullptr) {
            delete[] s.str;
            s.str = nullptr;
            s.len = 0;
        }
    // 写入
    char tmp[4096] = {0}; 
    is >> tmp;
    s.len = strlen(tmp);
    s.str = new char[s.len + 1];
    strcpy(s.str, tmp);
    return is;
}

int main(){
    MyString s1("123");    // 有参构造
    MyString s2 = "123a";  // 有参构造
    
    MyString s3;  // 无参构造
    s3 = "abc";   // 赋值操作符函数 - const char *p
   
    MyString s4 = s3;   // 拷贝构造
   
    MyString s5;
    s5 = s4;    // 赋值操作符函数 - const MyString &another
   
    MyString s6;  
    cin >> s6;    // 重载 >>
    cout << s6;   // 重载 <<
   
    char *p = nullptr;
    MyString s7(p);  // 有参构造
}

几点注意:

(1)临时对象与拷贝省略

MyString s2 = "123a"; 实际上是 MyString s2 = MyString("123a");MyString("123a") 定义一个临时对象,再拷贝给 s2

但实际上不发生拷贝构造,临时对象直接转正为 s2 ,原因是编译器优化了,造成了拷贝省略(copy-elision)。 见 Copy Elision 中的返回值优化和右值拷贝优化

(2)const 的修饰必不可少。

有参构造 MyString(const char *str) 、 拷贝构造 MyString(const MyString &another) 等函数的形参为只读,一定要用 const 修饰。

因为,如果实参是 const 修饰的,就无法完成传参数,const 类型不可以给非 const 类型传参。

(3)记得回收原空间。

构造函数拷贝构造函数 外,利用 赋值操作符重载 >> 函数 更改对象值的函数,要先回收对象原来申请的空间。

(4)判断参数是否特殊。

传入字符串,可能是空字符串 或者 nullptr。

传入对象,可能是自身。常见于 赋值操作符

四、继承

1.类和类的关系

追求:高内聚,低耦合。

(1)has A

类B has 类A,类B 依赖于 类A。耦合度教大。

class A {
  // ...
};

class B {
private:
  A a;
  // ...
};

(2)use A

类B use 类A,类B 依赖于 类A。耦合度较小。

class A {
	// ...
};

class B {
public:
  void fun(A &a){
    // ...
  }
  //...
};

(3)is A

类B 继承于 类A。耦合度非常高。

class A {
	// ...
};

class B:public A {
  
};

2.继承的基本概念

(1)格式

class Student {
public:
  Student(int id, string name){
    this->id = id;
    this->name = name;
  }

private:
  int id;
  string name;
};

class StudentPlus : public Student {
public:
  StudentPlus(int id, string name, double score) : Student(id, name){  // 利用父类构造
    this->score = score;
  }
private:
  double score;
};

对于父类的变量,也可以用子类构造,比如:

StudentPlus(int id, string name, double score){
    this->id = id;
    this->name = name;
    this->score = score;
}

但,实际上,子类构造依然调用了父类的无参构造!并且要求父类的变量是 public 修饰。

(2)定义

从已有类产生新类 的过程就是类的派生。原类:基类、父类;新类:派生类、子类。

派生与继承,是同一种意义两种称谓。 is A 的关系。

(3)子类的组成

子类内部部分空间和父类空间一样。

2020-05-31-eX5LVm

(4)继承方式

父类的 public父类的 protected父类的 private
公有继承 public在子类中为 public在子类中为 protected子类不可见
保护继承 protected在子类中为 protected在子类中为 protected子类不可见
私有继承 private在子类中为 private在子类中为 private子类不可见
  • 任何继承方式,子类都不能直接使用父类的私有成员 。

  • 公有继承 public,在子类中访问控制权限保持不变。

  • 保护继承,子类继承的成员变量全变保护(除父类的 private)。

  • 私有继承,子类继承的成员变量全变私有(除父类的 private)。

  • 公有继承 public 常用。

(5)访问控制权限的判断

判断 子类成员变量 的访问控制权限,分三步。

  1. 类的内部还是外部?
  2. 子类的继承方式是什么?
  3. 该子类成员变量在父类的访问控制权限是什么?

3.继承中的构造和析构

(1)赋值兼容原则

  • 子类对象给父类赋值或初始化(反之不可)
  • 子类对象可以当父类对象使用
  • 父类指针可以指向子类对象(反之不可)
  • 父类引用可以引用子类对象(反之不可)
  • 子类对象给父类赋值或初始化(反之不可)

子类内存空间大,可以用内存大的给内存小的赋值,因为子类变量齐全。

class Father {
public:
  int f_num;
}

class Sun : public Father {
public:
  int s_num;
}

Sun s;
Father f1 = s;    // 用子类对象给父类对象初始化
Father f2;
f2 = s;       // 用子类对象给父类对象赋值
  • 子类对象可以当父类对象使用
  • 父类指针可以指向子类对象 (反之不可)

多态发生的必要条件

class Father {
public:
  int f_num;
  void Print(){
    cout << f_num << endl;
  }
}

class Sun : public Father {
public:
  int s_num;
}

Father *f_p = nullptr;
Sun *s_p = nullptr;

Sun s;

f_p = &s;   // 父类指针指向子类对象
f_p->Print();     // 但是父类指针只能操作子类继承的成员变量和成员方法
  • 父类引用可以引用子类对象(反之不可)
Sun s;
Father &pp = s;

(2)继承中构造析构调用原则

先构造父类,再构造成员变量、最后构造自己。

先析构自己,在析构成员变量、最后析构父类。

  • 调用子类构造之前,调用父类无参构造或者,显示调用父类有参构造。见:样例
  • 析构与调用构造函数的顺序相反。

(3)子类父类重名变量和重名函数

  • 访问父类重名变量,用类作用域符。
class Father {
public:
    int a;
};

class Sun : public Father {
public:
    int a;
    void Print(){
        cout << Father::a << endl;    // 访问父类变量,用类作用域符
        cout << this->a << endl;
    }
};

(4)继承中的 static

  • 子类共享父类的静态成员变量。(整个家族共享一个静态成员变量)
  • 静态成员变量遵守继承的控制权限
class Father {
public:
	static int static_i;
};

int Father::static_i = 10;       // 静态成语变量的初始化,在类的外部初始化。

class Sun : public Father {
public:
  cout << Father::static_i;      // 子类访问静态成员变量的方式:类作用域符。
};

4.多继承

子类拥有同时继承多个父类,同时拥有多个父类的成员变量和成员函数。

class Soft {
public:
    int price;
    Soft(int price){
        this->price = price;
    }
    void Sit(){
        cout << "sit" << endl;
    }
};

class Bed {
public:
    int price;
    Bed(int price){
        this->price = price;
    }
    void Sleep(){
        cout << "Sleep" << endl;
    }
};

class SoftBed : public Soft, public Bed {    // 多继承的格式
public:
    int price;
    // 多继承的构造函数的格式
    SoftBed(int soft_price, int bed_price) : Soft(soft_price), Bed(bed_price){  
        this->price = soft_price + bed_price;
    }
    void SitAndSleep(){
        Sit();
        Sleep();
    }
};

5.虚继承

(1)多继承中二义性问题

如果一个派生类从多个基类派生,而这些基类又有一个共同 的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。

C类多继承于 B1类、B2类,B1类、B2类均继承于 A类,因此,C类将构造两次A类对象,所以拥有两个同样的A类对象成员变量,C类访问该变量将产生错误。

2020-06-04-QrhazV

class Furniture {
public:
    string texture;
};

class Soft : public Furniture {
public:
    void Texture(){
        cout << "Soft is " << texture << endl;
    }
};

class Bed : public Furniture {
public:
    void Texture(){
        cout << "Bed is " << texture << endl;
    }
};

class SoftBed : public Soft, public Bed {    // 多继承
public:
    void Texture(){
        cout << "Furniture is " << Furniture::texture << endl;   // Error,无法确定哪一个Furniture类的成员(有两个)
        cout << "Soft is " << Soft::texture << endl;
        cout << "Bed is " << Bed::texture << endl;
    }
};

(2)virtual

父类 继承 父类的父类 的继承方式之前,加上 virtual。

目的:防止子类多继承父类的时候,出现父类的父类中的变量拷贝多份。

class Furniture {
public:
    string texture;
};

class Soft : virtual public Furniture {  // 虚继承
public:
    void Texture(){
        cout << "Soft is " << texture << endl;
    }
};

class Bed : virtual public Furniture {   // 虚继承
public:
    void Texture(){
        cout << "Bed is " << texture << endl;
    }
};

class SoftBed : public Soft, public Bed {    // 多继承
public:
    void Texture(){
        cout << "Furniture is " << Furniture::texture << endl;   // 正确!不会出现二义性
        cout << "Soft is " << Soft::texture << endl;
        cout << "Bed is " << Bed::texture << endl;
    }
};

五、多态

1.多态的意义

多态现象:几个似而不同的几个对象,收到同一个信号时,执行不同的操作。

多态的优点: 增加系统灵活性,减轻系统升级维护调试的工作量和复杂度。(未来依旧可以使用以前的架构。)

2.多态的三个必要条件

  1. 有继承

  2. 有虚函数重写

  3. 父类指针或引用 指向子类对象

    子类指针指向子类对象时,无论子类的重定义还是虚函数重写,都会调用子类的函数。

    父类指针指向子类对象时,默认调用父类函数。

3.虚函数

virtual 关键字声明,分文件写类时,类外定义函数时,不用加 virtual 关键字,声明的时候需要加。

父类指针指向子类对象时,为了父类指针能够调用子类重写父类的函数,将父类的该函数标记为虚函数。

class Father {
public:
    int id;
    Father(int id){
        this->id = id;
    }
    virtual void Print(){               // virtual 定义虚函数
        cout << "Father " << this->id << endl;
    }
};

class Sun : public Father {    //(第1点:有继承)
public:
    int id;
    Sun(int id_sun, int id_father) : Father(id_father){
        this->id = id_sun;
    }
    virtual void Print(){            // virtual 表示重写了父类的虚函数 //(第2点:有虚函数重写)
    // 此处的 virtual 可以不写,但是为了表示,此函数是重写父类的虚函数,所以加上 virtual
      cout << "Sun " << this->id << endl; 
    }
};

void Funcation(Father *p){    // 多态:用父类指针指向子类对象  //(第3点:父类指针或引用 指向子类对象)
    p->Print();
    return;
}

int main(){
    Sun s(2, 3);
    Funcation(&s);   // 传递子类对象指针。
    return 0;
}

4.静态联编和动态联编

  • 联编是程序模块和代码之间相互关联的过程。
  • 静态联编(sta5c binding)在编译阶段 实现程序的关联和连接。重载函数就是静态联编。
  • 动态联编:在运行时,才进行程序的关联和连接。switch 语句和 if 语句 就是动态联编。

补充:

  • C++与C相同,是静态编译型语言。
  • 编译时,编译器根据指针类型,判断指向什么样的对象。(所以父类指针默认指向父类对象)
  • 在程序运行前,不知道父类指针是指向父类对象还是子类对象,为保证安全,编译器假设父类指针指向父类对象。这是静态联联编。
  • 多态发生就是动态联编。在运行的时候,才知道指针指向的是子类对象还是父类对象。

5.虚析构函数

问题: delete 只会调用父类的析构函数,不会调用子类的析构函数,造成内存泄漏。

因为 p 的类型是 Father,所以 delete p时,只能调用父类的析构函数 ~Father()。但实际上应该先析构 Sun, 再析构 Father。所以造成了子类未调用析构函数,造成了内存泄漏。

class Father{
public:
    Father(){
        char_p = new char[10];
        strcpy(char_p, "father");
        cout << "Father()" << endl;
    }
    virtual void Print(){
        cout << char_p << endl;
    }
    ~Father(){
        if (char_p != nullptr) {
            delete char_p;
            char_p = nullptr;
        }
        cout << "~Father()" << endl;
    }
private:
    char *char_p;
};

class Sun : public Father {
public:
    Sun() : Father(){
        char_p = new char[10];
        strcpy(char_p, "sun");
        cout << "Sun()" << endl;
    }
    virtual void Print(){         // 重写父类的虚函数 Print
        cout << char_p << endl;
    }
    ~Sun(){
        if (char_p != nullptr) {
            delete char_p;
            char_p = nullptr;
        }
        cout << "~Sun()" << endl;
    }
private:
    char *char_p;
};

void Fun(Father *p){    // 多态条件之一:父类指针 指向子类对象。
    p->Print();         // 此处发生多态,Sun中重写了虚函数 Print
    delete p;
  	// 因为 p 的类型是 Father,所以 delete p,只能调用 ~Father()。
    // 应该先析构 Sun, 再析构 Father。
    // 所以造成了内存泄漏
}

int main(){
    Sun *sun_p = new Sun;
    Fun(sun_p);
}

解决方案: 使用虚析构函数。

在父类的析构函数前面加上 virtual

当父类指针指向子类对象时,并且该指针将要被 delete 时,先调用子类的析构,再调用父类的析构(因为执行子类的析构之后,默认会执行父类的析构)。

class Father{
public:
    Father(){
        char_p = new char[10];
        strcpy(char_p, "father");
        cout << "Father()" << endl;
    }
    virtual void Print(){
        cout << char_p << endl;
    }
    virtual ~Father(){                  // 虚析构函数
        if (char_p != nullptr) {
            delete char_p;
            char_p = nullptr;
        }
        cout << "~Father()" << endl;
    }
private:
    char *char_p;
};

class Sun : public Father {
public:
    Sun() : Father(){
        char_p = new char[10];
        strcpy(char_p, "sun");
        cout << "Sun()" << endl;
    }
    virtual void Print(){
        cout << char_p << endl;
    }
    virtual ~Sun(){                     // virtual 写不写都行,表示父类的析构函数为虚析构函数。
        if (char_p != nullptr) {
            delete char_p;
            char_p = nullptr;
        }
        cout << "~Sun()" << endl;
    }
private:
    char *char_p;
};

void Fun(Father *p){
    p->Print();
    delete p;            // 先调用 子类的析构,再调用 父类的析构,最后 delete p所指向的内存。
}

int main(){
    Sun *sun_p = new Sun;
    Fun(sun_p);
}

6.重载、重写、重定义

(1)重载

  • 重载一定是同一个作用域下。

  • 函数名相同,参数列表不一样。

(2)重定义

  • 发生在不同的类中。

有两种:

  1. 普通函数重定义(屏蔽基类函数)
    • 函数同名,参数列表相同,且父类该函数无 virtual ,父类该函数被隐藏。
    • 函数同名,参数列表不同,无论父类该函数有无 virtual,父类该函数被隐藏。
  2. 虚函数重写
    • 函数同名,参数列表相同,且父类该函数有 virtual
    • 子类 覆盖重写了 父类的 虚函数。
    • 此时发生多态。

代码演示:重定义

class Father {
public:
    void Print(){
        cout << "Father " << endl;
    }
};

class Sun : public Father {
public:
    void Print(){             // 函数同名,参数列表相同,且基类函数无 virtual ,父类该函数被隐藏。
        cout << "Sun " << endl; 
    }
};

int main(){
    Sun s;
    s.Print();   // 打印结果:Sun 
    return 0;
}

7.多态的原理

(1)虚函数表和 vptr 指针

  • 当类中声明虚函数时,编译器自动在类中生成一个虚函数表,虚函数表中存放虚函数的指针(函数的入口地址)。
  • 存在虚函数表时,每个对象都有一个 vptr 指针,指向虚函数表。
  • 虚函数表在常量区中。

当用父类指针指向子类对象,并调用 Fun(int, int) 函数时,可能会发生的情况:

  1. 父类中无 Fun(int, int) 函数:

    报错!父类指针只能调用子类从父类继承的函数。

  2. 父类中 Fun(int, int) 函数声明为虚函数【动态联编】:

    • 通过 VPTR 指针,在子类的虚函数表中查找 Fun(int, int) 函数。
    • 若找到 Fun(int, int) 函数,执行虚函数表中的 Fun(int, int) 函数!
    • 若没找到 Fun(int, int) 函数,执行父类中的 Fun(int, int) 函数。
  3. 父类中 Fun(int, int) 函数为普通函数【静态联编】:

    直接执行父类中的 Fun(int, int) 函数。(不会查询虚函数表)

(2)验证 VPTR 指针的存在

  • 存疑】当加入 虚函数 时,由 原来的 4 字节 变成了 16 字节。

    按理说,应该增加 8 个字节,VPTR 指针的内存算入在类的内存中,函数 和 虚函数表的内存不算入类的内存。

  • 原因】由于字节对齐的原因。

    • 类中没有变量时,内存大小为 1 字节。
    • 字节对齐以类型最长的变量为准。例如下方代码中,以 8 字节的指针为准对齐。变成了 2 * 8 = 16。
class Father {
public:
    virtual void Fun(){
        cout << "Father:Fun()" << endl;
    }
private:
    int a;
};

class Sun {
public:
    void Fun(){
        cout << "Sun:Fun()" << endl;
    }
private:
    int a;
};

int main(){
    cout << "Father: " << sizeof(Father) << endl;   // Father: 16
    cout << "Sun: " << sizeof(Sun) << endl;         // Sun: 4
}

(3)VPTR 指针的分步初始化

  1. 创建对象时,编译器对 VPTR 指针进行初始化。

  2. 当对象调用父类的构造函数时,VPTR 指针指向父类对象的虚函数表。

  3. 当执行完父类构造函数后,再执行子类的构造函数时,VPTR 指针指向 子类对象的虚函数表。

class Father {
public:
    Father(int a){
        this->a = a;
        Fun();       // 父类的构造函数中,VPTR指针,指向父类的虚函数表。
    }
    virtual void Fun(){
        cout << "Father: virtual" << endl;
    }

private:
    int a;
};

class Sun : public Father {
public:
    Sun(int a, int b) : Father(a){
        Fun();         // 一旦父类构造函数构造完,VPTR指针指向子类的虚函数表
        this->b = b;
    }
    virtual void Fun(){
        cout << "Sun: virtual" << endl;
    }

private:
    int b;
};

(4)父类指针和子类指针步长

  • 父类指针的步长为父类的大小。
  • 子类指针的步长为子类的大小。
  • 指针不发生多态。

PS:如果用父类指针操作子类对象,并且自增,没发生错误,可能原因是:

  • 子类没有新的变量,所以子类父类大小相同。
  • 由于字节对齐的原因:子类中有两个 int 变量,父类中只一个 int 变量,但两个类的内存大小相同。(比如下面代码中的两个类。)
class Father {
public:
    Father(int a){
        this->a = a;
    }
    virtual void Fun(){
        cout << "Father: virtual" << endl;
    }

private:
    int a;
};

class Sun : public Father {
public:
    Sun(int a, int b) : Father(a){
        this->b = b;
    }
    virtual void Fun(){
        cout << "Sun: virtual" << endl;
    }

private:
    int b;
    int c;
};



int main(){
    Sun s[3] = {Sun(1, 2), Sun(2, 4), Sun(4, 8)};
    Father *p = &s[0];
    p++;                   // 虽然父类指针指向子类对象,但是指针步长依然是父类的大小。【不能++】
    p->Fun();              // 发生段错误:segmentation fault
}

(5)多态的总结

  • 多态的实现效果

    同样的调用语句,有不同的表现形式。

  • 多态实现的三个条件

    继承,虚函数重写,父类指针指向子类对象。

  • 多态的 C++ 实现

    虚函数表和 VPRT 指针。

    virtual 关键字告诉编译器,该函数支持多态。

    不是根据指针类型来判断如何调用,而是根据指针所指的实际对象类型来判断如何调用。

  • 多态的理论依据

    动态联编,根据实际的对象类型来判断重写函数的调用。

  • 多态的重要意义

    设计模式的基础,框架的基石。

8.纯虚函数和抽象类

纯虚函数:

  • 基类中声明的虚函数,且没有定义,等待派生类重写此虚函数。
// 拥有虚函数的抽象类
class Abstract {
public:
  virtual void Print() = 0;   // 纯虚函数的声明。
};

// 继承抽象类且重写虚函数的子类
class Sun : public Abstract{
public:
  virtual void Print(){
    cout << "重写虚函数" << endl;
  }
};

抽象类:

  • 无论有没有变量,只要有纯虚函数的类就是抽象类。
  • 一个类继承抽象类,如果没有重写纯虚函数,这个类还是抽象类。
  • 抽象类不能实例化对象。

9.纯虚函数和多继承

继承多个接口(抽象类)。

class Interface01 {
public:
    virtual void Fun01() = 0;
};

class Interface02 {
public:
    virtual void Fun02() = 0;
};

class Child : public Interface01, public Interface02 {
public:
    virtual void Fun01(){ cout << "Interface01" << endl; }
    virtual void Fun02(){ cout << "Interface02" << endl; }
};

六、常见错误

1. pointer being freed was not allocated

错误信息:

malloc: *** error for object 0x7fd055c05820: pointer being freed was not allocated
malloc: *** set a breakpoint in malloc_error_break to debug

指针指向堆区内存,该指针被释放了两次。

char *p = new char(‘a’);
delete p;
delete p;

本博客所有文章均个人原创,除特别声明外均采用 CC BY-SA 4.0协议,转载请注明出处!

 目录

致敬李文亮及各路英雄好汉!