结构体与class

class与struct的唯一区别就是默认的访问权限不同。struct默认访问权限是public;class默认访问权限是private。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Sales_data.h
#ifndef SALES_DATA_H
#define SALES_DATA_H // 为避免命名冲突,一般预处理变量全部大写
#include <string> // 预处理器看到 #include 标记时会将指定的头文件内容拷贝进来代替 #include
using namespace std;

struct Sales_data {
string bookNo; // 未提供类内初始值,创建对象时,默认初始化为空字符串
unsigned units_sold = 0; // 可提供类内初始值,用于创建对象时初始化
double revenue = 0.0;
}; // 分号结尾
#endif

// main.cpp
#include "Sales_data.h"

int main() {
Sales_data accum, trans, *salesptr;
}
  • 为确保各文件中类定义一致,类通常定义在头文件中。头文件名应与类名一致。

  • 头文件通常包含只能定义一次的实体,如类、const、constexpr变量等。

  • 头文件多次包含:

    • 使用预处理器,确保头文件被多次包含仍能正常工作。

    • #define把一个名字设定为预处理变量;#ifdef当且仅当变量已定义时为true;#ifndef当且仅当变量未定义时为true;一旦检查为真,则执行后续操作直至遇到#endif为止。

    • 所有头文件都应加上头文件保护符,无论是否需要。

    • 未加头文件保护符:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // file1.h
      class file1{};

      // file2.h
      #include "file1.h"
      class file2{};

      // file3.h
      #include "file1.h"
      #include "file2.h"


      // file3.h实际的展开内容是这样的:
      class file1{};
      class file1{};
      class file2{};
    • 加头文件保护符:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      // file1.h
      #ifndef _FILE1_H_
      #define _FILE1_H_

      class file1{};
      #endif


      // file2.h
      #ifndef _FILE2_H_
      #define _FILE2_H_
      #include "file1.h"

      class file2{};
      #endif


      // file3.h
      #ifndef _FILE3_H_
      #define _FILE3_H_
      #include "file1.h"
      #include "file2.h"

      #endif


      // file3.h实际的展开内容是这样的:
      // 通过 file2.h 的 #include "file1.h" 第二次包含file1.h时,会被file1.h中的#ifndef _FILE1_H_判为false退出
      class file1{};
      class file1{};
  • 成员函数的声明必须在类的内部,它的定义既可以在类的外部,也可以在类的内部(隐式inline函数)。

  • 定义在类内部的成员函数是自动inline的。(若成员函数想在类外部定义且内联,需在类外定义处手动指定inline关键字,类内声明处无需inline关键字)

  • 成员函数通过额外隐式参数 this来访问调用它的对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct Sales_data{
    string bookNo;

    double avg_price() const;

    // const成员函数,常成员函数
    string isbn() const { return bookNo; } // 等价 return this.bookNo;
    };

    double Sales_data::avg_price() const
    {
    return 233.33;
    }

    Sales_data total;
    /*
    伪代码,等价于 string res = total.isbn(&total);
    编译器负责把total对象的地址传递给isbn的隐式形参this
    this形参是隐式定义的,任何自定义this参数或变量的行为都是非法的
    因this总指向调用对象,所以this是一个常量指针,即不允许改变this指向的地址
    */
    string res = total.isbn();
  • 默认情况下,不能把this绑定到一个常量对象上,因为通过this可修改该对象。即不能在一个常量对象上调用普通成员函数。(常量对象,以及常量对象的引用或指针,只能调用常成员函数)

  • 上述情况应使用const成员函数,const的作用是将this指针类型,从默认的 “常量指针“ 改为 ”指向常量的常量指针“。this的类型从 Sales_data *const 转为 const Sales_data *const。此时 isbn() 不可修改this所指对象。

  • 对于const成员函数,函数声明和外部该函数定义都需使用const关键字。

  • 编译器分两步处理类:首先编译成员的声明,然后是成员函数。因此成员函数体可以随意使用类中的其他成员,无需在意这些成员出现的次序

  • 构造函数

    • 没有返回类型。可在类内定义,也可在类外定义。
    • 不同构造函数间必须在参数数量和参数类型上有所区别。
    • 构造函数不能声明为const(创建一个对象,构造完成后才有机会取得常量属性);构造函数在const对象的构造过程中可以向其写值。
    • 如果不显示定义任何一个构造函数,那么编译器自动隐式定义一个默认构造函数(合成的默认构造函数)。默认构造函数无任何参数。
    • 注意,如果类成员包含内置类型对象复合类型(比如数组和指针)对象其他类类型的对象,则编译器自动生成的合成默认构造函数很可能无法对它们进行合理的初始化,导致它们的值未定义。对于这种类,一般需要自己手动定义合理的默认构造函数
    1
    2
    3
    4
    5
    struct Sales_data{
    string bookNo;
    Sales_data() = default; // 等价于 Sales_data() {}
    Sales_data(const string &s): bookNo(s);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct Sales_data {
    double revenue = 0.0;

    Sales_data(double temp2) {
    this->revenue = temp2;
    }
    };

    int main() {
    // Sales_data sales_data = Sales_data(); // 报错,由于你已自定义一个构造函数,编译器不会再为你自动定义默认构造函数了,如有需要你必须手动自定义

    Sales_data sales_data = Sales_data(2.5);
    return 0;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct Sales_data {
    string bookNo;

    Sales_data(const string &temp) {
    this->bookNo = temp;
    }
    };

    int main() {
    string temp = "2333";
    Sales_data sales_data = Sales_data(temp);
    string res_1 = sales_data.bookNo;
    temp = "45678";
    string res_2 = sales_data.bookNo; // 还是 2333, 虽传参是引用,但 this->bookNo = temp; 是拷贝操作
    return 0;
    }
    • 构造函数初始值列表
    1
    2
    3
    4
    5
    6
    7
    Sales_data(const string &s): bookNo(s){}

    /*
    构造函数初始值列表只能说明用于初始化成员的值,不限定初始化的执行顺序
    成员的初始化顺序与它们在类定义中出现的顺序一致
    */
    Sales_data(const string &s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p*n){}
    • 类的初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 变量的定义、初始化、赋值
    string foo = "Hello World!"; // 定义并初始化
    string bar; // 定义并默认初始化为空string对象
    bar = "Hello World!"; // 为bar赋值

    /*
    对于类,构造函数初始化列表为初始化操作,构造函数体内为赋值操作
    如果成员是const或引用的话,必须对成员进行初始化
    如果成员是某种类类型,且该类没有定义默认构造函数,也必须将这个成员初始化
    */
    class ConstRef {
    public:
    ConstRef(int ii);
    private:
    int i;
    const int ci;
    int &ri;
    };

    ConstRef::ConstRef(int ii) { // 编译都不通过
    i = ii; // 正确
    ci = ii; // 错误,不能给const赋值
    ri = i; // 错误,ri没被初始化
    }

    ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(i) {} // 正确
    • 如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Sales_data {
    private:
    string str;
    int num;
    public:
    Sales_data(string s = "2333", int i = 10) :str(s), num(i) {}
    };


    int main() {
    Sales_data sales_data; // 正确 str = 2333, num = 10
    Sales_data sales_data_2("5555", 20);
    return 0;
    }
    • 委托构造函数

      依次先执行受委托构造函数的初始值列表和函数体,然后再执行委托人函数的函数体。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Sales_data {
    private:
    string str;
    int num;
    double price;
    public:
    Sales_data(string s, int i, double j) : str(s), num(i), price(j) { printf("2333\n"); }
    Sales_data() : Sales_data("", 0, 0) { printf("555555\n"); }
    Sales_data(string s) : Sales_data(s, 0, 0) { printf("66666\n"); }
    };

    int main() {
    Sales_data sales_data; // 2333 555555
    return 0;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 第一种方式,显式调用构造函数,在栈上分配内存,系统自动进行分配和释放
    A a = A();
    A a = A(1);

    // 第二种方式,隐式调用构造函数,在栈上分配内存,系统自动进行分配和释放
    A a;
    A a(1);

    // 构造函数还可以与new一起使用,在堆中动态分配内存,需自己手动进行释放
    A a = new A(); // 记得delete a
    A a = new A(1); // 记得delete a
    • 聚合类

      符合下述条件:

      • 所有成员都是public
      • 没有定义任何构造函数
      • 没有类内初始值
      • 没有基类,没有virtual函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct Data {
    string str;
    int val_1;
    int val_2;
    };


    int main() {
    // 聚合类可以通过花括号的形式初始化成员并创建对象
    // 初始值的顺序必须与成员声明的顺序一致
    Data data_1 = { "str1", 10, 20 };

    // 若初始值的个数小于成员的个数,则后面的成员被默认初始化
    Data data_2 = { "str2", 30 }; // val_2被默认初始化为0
    return 0;
    }
  • 拷贝、赋值和析构

    • 对象会在如下情况下被拷贝:初始化变量、以值的方式传递或返回一个对象。
    • 赋值:使用赋值运算符。
    • 析构:对象销毁。
  • 如果不主动定义这些操作,编译器会替我们合成拷贝、赋值、析构操作。

  • 某些类使用编译器合成的版本可能无法正常工作。例如类使用了动态分配的内存资源。管理动态内存的类通常不能依赖于编译器合成版本的操作。

  • 若类中只包含vector、string成员,则编译器的合成版本能够正常工作。

  • 友元

    • 类允许其他类或函数访问它的非公有成员。
    • 友元声明只能出现在类定义的内部,但具体位置不限。
    • 友元不是类的成员,不受访问控制符的约束。
    1
    2
    3
    4
    5
    6
    7
    class Sales_data{
    //为非成员函数add做友元声明
    friend Sales_data add(const Sales_data&, const Sales_data&);
    }

    // 非成员函数add的声明
    Sales_data add(const Sales_data&, const Sales_data&);
    • 友元类
    1
    2
    3
    4
    5
    class Screen{
    friend class Window_mgr; // 友元类Window_mgr的成员函数可以访问Screen类的所有成员,包括private成员
    // ...
    }
    // 注意,友元关系不存在传递性
    • 成员函数友元
    1
    2
    3
    4
    5
    class Screen{
    friend void Window_mgr::clear(ScreenIndex);
    // ...
    }
    // 如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别进行友元声明
    1
    2
    3
    4
    5
    // 就算在类内部对友元函数进行定义,一般也需要在类的外部提供相应的函数声明,使得函数可见
    struct X{
    friend void f() { /*函数实现*/ };
    };
    void f(); // 在类的外部提供相应的函数声明
  • 返回*this的成员函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class Sales_data {
    private:
    int val = 3;
    public:
    Sales_data& combine(const Sales_data &rhs) {
    this->val += rhs.val;
    return *this;
    }

    void combine_2(const Sales_data &rhs) {
    this->val += rhs.val;
    return;
    }

    const Sales_data& combine_3(const Sales_data &rhs) const {
    printf(" const func cannot change val ");
    return *(this);
    }
    };


    int main() {
    Sales_data sale_data_1;
    Sales_data sale_data_2;
    Sales_data sale_data_3;

    sale_data_1.combine(sale_data_2).combine(sale_data_3); // 返回void的combine_2是没办法做连续操作的

    sale_data_1.combine_3(sale_data_2).combine_3(sale_data_3); // 正确
    // sale_data_1.combine_3(sale_data_2).combine(sale_data_3); // 报错,combine_3返回的const的引用无权调用非常成员函数combine,因combine可能修改const引用的内容
    return 0;
    }
  • 基于const的重载

    常量对象只能调用常成员函数;非常量对象能调用常成员函数与非常成员函数,但是后者是更好的匹配

    据此,可实现基于const的重载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Sales_data {
    public:
    void func() {
    printf(" normal func called \n");
    }
    void func() const {
    printf(" const func called \n ");
    }
    };



    int main() {
    Sales_data sales_data_1;
    const Sales_data sales_data_2;

    sales_data_1.func(); // normal func called
    sales_data_2.func(); // const func called
    return 0;
    }
  • 类的不完全类型

    • 类在声明之后,定义之前,是一个不完全类型。
    • 可以定义指向这种类型的指针或引用,也可以声明(但不能定义)以不完全类型作为参数或返回类型的函数。
    • 因为只有当类全部完成后类才算被定义,所以一个类的成员类型不能是该类自己。
  • 外围作用域的查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int val = 50;

class Test {
private:
int val = 10;
public:
int getVal() {
return ::val; // 全局的val
}
};


int main() {
Test test;
int res = test.getVal(); // 50
return 0;
}
  • 类的静态成员

    • 静态成员可以是public、private。静态数据成员的类型可以是常量、引用、指针、类类型。
    • 静态成员函数不包括this指针。
    • 静态成员函数不能声明为const
    • 尽量使用类的作用域运算符访问静态数据成员,但使用类对象、引用、指针去访问也允许。
    • 成员函数不用通过类的作用域运算符就能直接使用静态成员。
    • 类似全局变量,静态数据成员定义在任何函数之外。一旦被定义,将一直存在于程序的整个生命周期中。
    • 静态数据成员可以是不完全类型,因此静态数据成员的类型可以是该类自己。而非静态数据成员则只能声明为该类自己的指针或引用。
    1
    2
    3
    4
    5
    6
    class Bar {
    private:
    static Bar meme1; // 正确,静态成员可以是不完全类型
    Bar *mem2; //正确,指针或引用可以是不完全类型
    // Bar mem3; // 错误,数据成员必须是完全类型
    };
    • 静态成员可以作为默认实参,普通成员不可以。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Bar {
    private:
    static const int val_1 = 10;
    const int val_2 = 20;

    public:
    int getNum(int num = val_1) { return num; }
    // int getNum(int num = val_2) { return num; } // 报错
    };
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #ifndef STATIC_TEST_H
    #define STATIC_TEST_H

    class StaticTest {
    public:

    // static int varA = 233; // 编译器报错,静态非常量成员不可以在类内初始化(更不可能由构造函数初始化),其只能在类外初始化
    static const int varB = 244; // 静态常量成员可以在类内初始化(当然,也可以在类外)

    static int varC;
    static int varE;

    // static int func_const() const; // 编译器报错,静态成员函数不允许使用const

    static int func();
    static int func_inner() {
    return 266;
    }

    static int varD;
    };

    int StaticTest::varC = 277;
    // int StaticTest::varC = 288; // 和其他对象一样,一个静态数据成员只能定义一次

    int StaticTest::varE = func(); // 可正常调用,先声明后定义,因此无顺序的问题
    int StaticTest::varD = func(); // 等号右侧位于类的作用域之内了,不用写 StaticTest::func();

    // const int StaticTest::varB = 299; // 编译器报错,多次重定义varB

    int StaticTest::func() { // 在类的外部定义静态成员函数,不能重复static关键字,该关键字只能出现在类的内部
    return 255;
    }

    #endif // !STATIC_TEST_H
  • 拷贝构造函数

    • 如果一个构造函数第一个参数是自身类类型的引用(必须是引用,不然就死循环了;且几乎总是const的引用),且任何额外参数都有默认值,则此构造函数是拷贝构造函数。

    • 与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。

    • 每个成员的类型决定了它被如何拷贝:对类类型成员,会使用其拷贝构造函数来拷贝;内置类型成员直接拷贝。

    • 拷贝初始化

    1
    2
    3
    4
    5
    string dots(10, '.');	// 直接初始化
    string s(dots); // 直接初始化
    string s2 = dots; // 拷贝初始化
    string null_book = "9-999-99999-9"; // 拷贝初始化
    string nines = string(100, '9'); // 拷贝初始化
    • 拷贝初始化不仅在用=定义时发生,下列情况也会发生

      • 将一个对象作为实参传递给一个非引用类型的形参
      • 从一个返回类型为非引用类型的函数返回一个对象
      • 用花括号列表初始化一个数组或一个聚合类
  • 拷贝赋值运算符

    • 与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
    • 将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Sales_data {
    public:
    Sales_data(const Sales_data&);
    Sales_data& operator=(const Sales_data&); // 有返回值,属重载运算符,不是构造函数一类
    private:
    string bookNo;
    int units_sold = 0;
    double revenue = 0.0;
    };

    // 与Sales_data的合成拷贝构造函数等价
    Sales_data::Sales_data(const Sales_data &orig) : bookNo(orig.bookNo), units_sold(orig.units_sold), revenue(orig.revenue) {}

    // 与Sales_data的合成拷贝赋值运算符等价
    Sales_data& Sales_data::operator=(const Sales_data &rhs) {
    this->bookNo = rhs.bookNo;
    this->units_sold = rhs.units_sold;
    this->revenue = rhs.revenue;
    return *this;
    }
  • 拷贝构造函数与拷贝赋值运算符的区别

    • 是调用拷贝构造函数还是调用赋值运算符,主要看是否有新的对象实例产生。如果产生了新的对象实例,那就调用拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。

    参考链接1参考链接2

  • 析构函数

    • 释放对象使用的资源,销毁对象非static数据成员。
    • 无返回值,不接受参数,因此不能被重载。对于一个类,只有唯一一个析构函数。
    • 析构函数有一个函数体和一个析构部分(隐式的)。首先执行函数体,然后再销毁成员。成员按初始化顺序的逆序进行销毁。
    • 析构函数不存在类似构造函数的初始化列表去控制成员如何销毁,即析构部分只能是隐式的。
    • 与普通指针不同,智能指针是类类型,具有析构函数。因此智能指针成员在析构阶段会被自动销毁。
    • 成员销毁时发生什么完全依赖于成员的类型。销毁类类型成员需执行成员自己的析构函数,销毁内置类型无需任何操作。注意,隐式销毁一个指针类型的成员不会delete它所指向的对象,需在析构函数体手动delete
    • 系统何时调用析构函数
      • 当类类型变量离开作用域时,系统自动调用析构函数
      • 当类对象被销毁时,系统会销毁该类对象的各成员,系统对各成员中的类类型成员调用析构函数
      • 当容器或数组被销毁时,其各元素会被销毁,若元素为类类型对象,系统调用各元素的析构函数
      • 对于动态分配的类对象,对指向它的指针应用delete运算符时,系统调用该类对象的析构函数
      • 对于临时对象,当创建它的完整表达式结束时被销毁,系统调用临时对象的析构函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class Sales_data {
    public:
    int i = 10;
    ~Sales_data() {
    printf("called\n");
    }
    };


    void test() {
    Sales_data a;
    Sales_data *b = &a; // Sales_data对象始终只有a一个,只在栈中占了一份内存。b只不过是指向对象a的一个指针,因此析构函数只会调用一次。
    /*
    函数返回时执行内容:
    销毁指针b;
    销毁对象a,执行a的析构函数
    */
    }

    int main() {
    test();
    return 0;
    }
  • 三/五法则

    • 通常,对析构函数的需求要比对拷贝构造函数或拷贝赋值运算符更明显。如果一个类必须手动定义析构函数(一般因存在new动态分配的资源),我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    class HasPtr {
    private:
    string *ps;
    int i;
    public:
    HasPtr(const string str = "") : ps(new string(str)), i(0) {} // 构造函数

    HasPtr(const HasPtr&); // 手动定义拷贝构造函数

    HasPtr& operator=(const HasPtr&); // 手动定义拷贝赋值运算符

    ~HasPtr() { // 需手动定义析构函数,因系统默认的合成析构函数只会销毁指针,不会销毁new动态分配的空间
    delete ps;
    }

    HasPtr func(HasPtr);
    };

    HasPtr::HasPtr(const HasPtr &hp) { // 系统默认的合成拷贝构造函数只会拷贝指针,不会new一份新的动态内存空间。因此手动定义拷贝构造函数
    this->i = hp.i;
    this->ps = new string(*(hp.ps));
    }

    HasPtr& HasPtr::operator=(const HasPtr& hp) { // 同上,系统默认的合成拷贝赋值运算符只会拷贝指针,不会new一份新的动态内存空间。因此手动定义拷贝赋值运算符
    this->i = hp.i;
    this->ps = new string(*(hp.ps));
    return *this;
    }

    HasPtr HasPtr::func(HasPtr hp) {
    /*
    如果不手动定义拷贝构造函数,会析构异常。因不同类对象hp和ret的成员ps会指向同一动态内存空间。
    函数返回时会销毁hp和ret,对同一动态内存空间进行两次delete会发生未定义错误
    */
    HasPtr ret = hp;
    return ret;
    }


    int main() {
    HasPtr hp_1("value_1");
    HasPtr hp_2("value_2");
    hp_1.func(hp_2);
    return 0;
    }
    • 如果一个类需要手动定义一个拷贝构造函数,几乎可以肯定它也需要手动定义一个拷贝赋值运算符。反之亦然。
  • default

    显示要求编译器生成合成版本的方法。(只能对具有合成版本的成员函数使用=default,即默认构造函数或拷贝控制成员)

    1
    2
    3
    4
    5
    6
    7
    8
    class Sales_data {
    public:
    Sales_data() = default; // 类内使用=default将隐式声明为内联方法
    Sales_data(const Sales_data&) = default;
    Sales_data& operator=(const Sales_data&);
    ~Sales_data() = default;
    };
    Sales_data& Sales_data::operator=(const Sales_data&) = default; // 类外使用=default
  • delete

    • 定义删除的函数。

    • 与=default不同,=delete必须出现在函数第一次声明的时候。

    • 与=default不同,我们可以对任何函数指定=delete。

    • 一般没人delete析构函数,否则编译器不允许定义该类型的变量或创建该类的临时对象。而且如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。

    1
    2
    3
    4
    5
    6
    struct NoCopy {
    NoCopy() = default;
    NoCopy(const NoCopy&) = delete; // 删除拷贝构造函数
    NoCopy &operator=(const NoCopy&) = delete; // 删除拷贝赋值运算符
    ~NoCopy() = default;
    };
  • private拷贝控制

    • 将拷贝构造函数或拷贝赋值运算符等声明为private,从而阻止拷贝。
    • 不推荐使用,因为友元和成员函数仍可调用这些private方法。若只声明但不定义这些private方法,友元或成员函数调用它们时会导致链接错误
  • 根据上述知识,定义一个“行为像值的类”

    • 每个对象都有自己的一份深拷贝
    • 赋值运算符通常组合了析构函数和构造函数的操作,并且即使将一个对象赋予它自身,也能保证正确
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class HasPtr {
    public:
    HasPtr(const string &s = "") : ps(new string(s)), i(0) {}
    HasPtr(const HasPtr &p) : ps(new string((*p).ps)), i(p.i) {}
    HasPtr& operator=(const HasPtr&);
    ~HasPtr() { delete this->ps; }
    private:
    string *ps;
    int i;
    };

    HasPtr& HasPtr::operator=(const HasPtr &rhs) {
    auto newp = new string((*rhs).ps);
    delete this->ps;
    this->ps = newp;
    this->i = rhs.i;
    return *this;
    }
  • 根据上述知识,定义一个“行为像指针的类”

    • 令一个类展现类似指针的行为,最好用shared_ptr来管理类中的资源。
    • 或者也可以使用引用计数。(引用计数应保存在动态内存中,而不能直接作为HasPtr对象的成员,问题如下:)
    1
    2
    3
    4
    HasPtr p1("Hiya");
    HasPtr p2(p1); // p1和p2指向相同的string
    HasPtr p3(p1); // p1、p2、p3指向相同的string
    // 如果引用计数保存在每个对象中,当创建p3时如何更新?可以递增p1中的计数器并将其拷贝到p3中,但p2中的计数器没办法更新。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    class HasPtr {
    public:
    HasPtr(const string &s = "") : ps(new string(s)), i(0), use(new int(1)) {}
    HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) {++(*(this->use))}
    HasPtr& operator=(const HasPtr&);
    ~HasPtr();
    private:
    string *ps;
    int i;
    int *use; // 保存在动态内存中的引用计数器
    };

    HasPtr::~HasPtr() {
    --(*(this->use)); // 析构函数在销毁动态内存时,需判断是否有其他对象仍指向这块内存
    if (*(this->use) == 0) {
    delete this->ps;
    delete this->use;
    }
    }

    HasPtr& HasPtr::operator=(const HasPtr &rhs) {
    ++(*(rhs.use)); // 递增右侧对象的引用计数
    --(*(this->use)); // 递减本对象的引用计数

    if (*(this->use) == 0) {
    delete this->ps;
    delete this->use;
    }

    this->ps = rhs.ps;
    this->i = rhs.i;
    this->use = rhs.use;
    return *this;
    }
  • 自定义swap

    如果一个类定义了自己的swap方法,算法将优先使用自定义版本的swap,否则算法将使用标准库版本的swap方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class HasPtr {
    public:
    HasPtr(string s = "", int num = 0) : ps(new string(s)), i(num) {}
    friend void swap(HasPtr&, HasPtr&);
    private:
    string *ps;
    int i;
    };

    inline void swap(HasPtr &lhs, HasPtr &rhs) {
    swap(lhs.ps, rhs.ps); // 直接交换指针,避免中间string的创建 (这里的swap是std::swap)
    swap(lhs.i, rhs.i);
    }


    int main() {
    HasPtr s1("aaa", 1);
    HasPtr s2("bbb", 2);
    swap(s1, s2); // 调用自定义的swap方法
    return 0;
    }
  • 重载运算符

    • 对于二元运算符,第一个参数为左侧运算对象,第二个参数为右侧运算对象。

    • 如果一个运算符函数是类成员函数,第一个运算对象为隐式this指针。

    • 对于一个运算符函数,它或者是类的成员,或者至少含有一个类类型参数(即当运算符作用于内置类型时,我们无法改变运算符的含义)

    • 只能重载已有的运算符,无权发明新的运算符号。

    • 类似符号(+、-、*、&),既是一元运算符也是二元运算符,重载时根据参数的数量判断定义的是哪种运算符。

    • 不能被重载的运算符(:😃、(.*)、(.)、(? 😃

      1
      int operator+(int, int);	// 错误,不能为int重载运算符
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    // 将运算符定义为类成员函数,或普通非类成员函数

    class Test {
    private:
    int i;
    int j;
    public:
    Test(int i = 0, int j = 0) : i(i), j(j) {}
    Test& operator+=(const Test&);
    friend Test operator+(const Test&, const Test&);
    };

    // 重载成员运算符函数,首个参数为隐式this指针
    Test& Test::operator+=(const Test& test) {
    this->i += test.i;
    this->j += test.j;
    return *this;
    }

    // 重载非成员运算符函数,没有隐式this参数
    Test operator+(const Test& t_1, const Test& t_2) {
    int sum_i = t_1.i + t_2.i;
    int sum_j = t_1.j + t_2.j;
    Test res(sum_i, sum_j);
    return res;
    }



    int main() {
    Test t_1(1, 2);
    Test t_2(3, 4);

    Test t_3 = t_1 + t_2;
    Test t_4 = operator+(t_1, t_2); // 两个 + 调用方式等价

    t_1 += t_2;
    t_1.operator+=(t_2); // 两个 += 调用方式等价

    return 0;
    }
    • 详见C++ Primer 第14章…
  • 面向对象程序设计(OOP):数据抽象、继承、动态绑定

  • 下文中出现的“覆盖”、“隐藏”、“重载”是三个不同的概念,请用心体会。)

  • 基类将**”类型相关的函数“”派生类不做改变,直接继承的函数“区分对待。对于前者,基类希望它的派生类各自定义适合自身的版本,此时基类将这些函数声明为虚函数**。

    • 派生类通过类派生列表明确指出它是从哪个(哪些)基类继承而来的。
      • 继承基类时可使用访问说明符private,protected,public。
    • 派生类必须在其内部对所有重新定义的虚函数进行声明。p527(+p528,谁说派生类必须覆盖的?)(+p529 派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明?)(+p530,派生类可覆盖它继承的虚函数,但若派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他普通成员,派生类会直接继承其在基类中的版本)(+p536 所有虚函数都必须有定义?测试发现完全可以搞一个类但虚函数仅声明无定义,但main函数无该类的对象。这时也可以正常运行程序)(+p537 派生类虚函数覆盖,Base_2与Derived_2,Base_3与Derived_3,可用override进行辅助控制,+p538)
    • 派生类可在这些函数前继续加virtual关键字(可加可不加)。如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
    • 派生类可在形参列表后使用override关键字,显示注明使用该成员函数改写基类的虚函数(可加可不加,加就加在函数声明的末尾;非虚函数不可使用override关键字修饰;虚函数声明处可使用override关键字,但该虚函数定义处不可使用override关键字)。
    • 任何构造函数之外的非静态函数都可以是虚函数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Quote {
    public:
    string isbn() const;
    virtual double net_price(size_t) const;
    };


    class Bulk_quote : public Quote {
    public:
    double net_price(size_t) const override;
    };
  • 使用基类的指针或引用调用一个虚函数时将发生动态绑定

    因上述过程函数的运行版本由实参决定,即在运行时选择函数的版本,所以动态绑定又被称为运行时绑定

    静态类型:编译时确定,是变量声明时的类型;动态类型:运行时确定,是变量内存中的对象类型。

    如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型保持一致。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 假设新增一个成员函数
    double Bulk_quote::print_total(ostream &os, const Quote &item, size_t n) {
    double ret = item.net_price(n); // 动态绑定,调用Quote::net_price或Bulk_quote::net_price
    os << "ISBN: " << item.isbn() << " # sold: " << n << " total due: " << ret << endl; // 非虚函数,派生类直接继承isbn函数,因此只能调用Quote::isbn
    return ret;
    }

    // basic的类型是Quote;bulk的类型是Bulk_quote
    print_total(cout, basic, 20); // 调用Quote的net_price
    print_total(cout, bulk, 20); // 调用Bulk_quote的net_price
  • 通过例子进行说明,这里我们完成Quote类的定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Quote {
    public:
    Quote() = default;
    Quote(const string &book, double sales_price) : bookNo(book), price(sales_price) {}

    string isbn() const { return bookNo; };

    // 返回给定数量的书籍的销售总额
    // 派生类负责改写并使用不同的折扣算法
    virtual double net_price(size_t n) const { return n * price; };

    virtual ~Quote() = default; // 对析构函数动态绑定

    private:
    string bookNo; // 书籍的ISBN编号
    protected:
    double price = 0.0; // 普通状态下不打折的价格
    };
    • 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。(详见下文解释)
    • 成员函数若未被声明为虚函数,则其解析过程发生在编译时而非运行时。对于isbn成员,其执行过程与派生类细节无关,不管作用于Quote对象还是Bulk_quote对象,isbn函数的行为都一样。在继承层次关系中只有一个isbn函数,因此不存在调用isbn()时执行哪个版本的问题。
  • 派生类继承基类的所有成员,但不一定有权限访问它们。派生类能够访问基类public,protected成员,不能访问private成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class A {
    private:
    int a_1 = 1;
    protected:
    int a_2 = 2;
    public:
    int a_3 = 3;
    };


    class B : public A {
    public:
    void testFun_1() {
    // int b_1 = a_1; // 编译不通过,派生类不可访问基类private成员
    int b_2 = a_2; // 正确,派生类可访问基类protected成员
    }
    };

    int main() {
    B b;
    // int test_2 = b.a_2; // 编译不通过,普通用户(使用类对象的用户),不可访问protected成员
    int test_3 = b.a_3; // 正确
    return 0;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 如果在类B中对类A的成员进行覆盖,结果如下。类A的定义同上
    class B : public A {
    public:
    int a_1 = 4;
    int a_2 = 5;
    int a_3 = 6;

    void testFun_1() {
    int b_2_1 = a_2; // 5
    int b_2_2 = A::a_2; // 2
    int b_2_3 = B::a_2; // 5
    return;
    }
    };

    派生类的友元可访问从基类继承的protected、public成员。

    派生类的成员和友元能通过派生类对象访问基类的protected成员,不能通过基类对象访问基类的protected成员。(详细内容参考C++ Primer p543)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    // 类A的定义同上
    class B : public A {
    public:
    void func_A(A a); // 参数为基类对象
    void func_B(B b); // 参数为派生类对象
    friend void func_fri_A(A a); // 参数为基类对象
    friend void func_fri_B(B); // 参数为派生类对象
    };

    void B::func_A(A a) {
    // int temp_1 = a.a_1; // 编译报错
    // int temp_1_1 = a.A::a_1;

    // int temp_2 = a.a_2; // 编译报错
    // int temp_2_2 = a.A::a_2;

    int temp_3 = a.a_3;
    int temp_3_3 = a.A::a_3;
    }

    void B::func_B(B b) {
    // int temp_1 = b.a_1; // 编译报错
    // int temp_1_1 = b.A::a_1;

    int temp_2 = b.a_2; // 正确,注意与main中int test_2 = b.a_2;的区别
    int temp_2_2 = b.A::a_2;

    int temp_3 = b.a_3;
    int temp_3_3 = b.A::a_3;
    }

    void func_fri_A(A a) {
    // int temp_1 = a.a_1; // 编译报错
    // int temp_1_1 = a.A::a_1;

    // int temp_2 = a.a_2; // 编译报错
    // int temp_2_2 = a.A::a_2;

    int temp_3 = a.a_3;
    int temp_3_3 = a.A::a_3;
    }

    void func_fri_B(B b) {
    // int temp_1 = b.a_1; // 编译报错,友元不能访问基类的private成员
    // int temp_1_1 = b.A::a_1;

    int temp_2 = b.a_2; // 友元可访问基类的protected成员
    int temp_2_2 = b.A::a_2;

    int temp_3 = b.a_3; // 友元可访问基类的public成员
    int temp_3_3 = b.A::a_3;
    }
  • 通过例子进行说明,这里我们完成Bulk_quote类的定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Bulk_quote : public Quote {
    public:
    Bulk_quote() = default;
    Bulk_quote(const string &, double, size_t, double);
    double net_price(size_t) const override; // 覆盖基类的函数版本,实现基于大量购买的折扣政策
    private:
    size_t min_qty = 0; // 适用折扣政策的最低购买量
    double discount = 0.0; // 以小数表示的折扣额
    };

    // 派生类对象的基类部分与派生类对象自己的数据成员,都是在构造函数初始化阶段执行初始化操作的
    // 派生类构造函数通过构造函数初始化列表,将实参传递给基类构造函数
    // 执行顺序:首先初始化基类部分,再执行基类构造函数体,然后按照声明的顺序依次初始化派生类成员,最后执行派生类构造函数体
    Bulk_quote::Bulk_quote(const string &book, double p, size_t qty, double disc) : Quote(book, p), min_qty(qty), discount(disc) {}

    double Bulk_quote::net_price(size_t cnt) const {
    if (cnt >= min_qty)
    return cnt * (1 - discount) * price;
    else
    return cnt * price;
    }
    • 派生类构造函数
      • 每个类控制它自己的成员初始化过程。即派生类不能直接初始化从基类继承而来的成员,派生类必须使用基类的构造函数来初始化它基类的部分。(虽然从语法层面你也可以在派生类构造函数体内,为它的public或protected基类成员赋值,但不建议这样操作,因为这样操作违背了“每个类负责定义各自接口”的概念)
  • 继承与静态成员

    • 静态成员同样遵循通用的继承访问规则。派生类可访问public和protected的基类静态成员,不可访问private的基类静态成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base {
public:
static void statmem();
};
void Base::statmem() { printf("23333"); }


class Derived : public Base {
void f(const Derived&);
};
void Derived::f(const Derived &derived_obj) {
Base::statmem(); // 通过基类访问
Derived::statmem(); // 通过派生类访问
derived_obj.statmem(); // 通过派生类对象访问
this->statmem(); // 通过this访问
}

/*
关于名字冲突与继承(C++ Primer p548),派生类可以重用定义在其直接或间接基类的名字,进而隐藏直接或间接基类的内容。如若希望访问基类中因派生类重用名字而被隐藏的内容,可通过作用域运算符来访问。
但是,除了覆盖继承而来的虚函数外,派生类最好不要重用其他定义在基类中的名字。(C++ Primer p549)
class Derived : public Base {
public:
static void statmem(); // 重用名字
void f(const Derived&);
};

void Derived::statmem() { printf("4444444444"); }

void Derived::f(const Derived &derived_obj) {
Base::statmem(); // 23333 通过基类作用域运算符访问
Derived::statmem(); // 4444444444 隐藏基类内容
derived_obj.statmem(); // 4444444444 隐藏基类内容
this->statmem(); // 4444444444 隐藏基类内容
}
*/
  • 被用作基类的类不能仅声明无定义;一个类不能派生它自己。

  • 拒绝继承,final关键字:

    • 类拒绝后续被继承,类名后添加关键字final。

      1
      class NoDerived final {...}
    • 虚函数拒绝后续被覆盖,虚函数后添加关键字final(虚函数声明处可使用final关键字,但该虚函数定义处不可使用final关键字)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class Base_3 {
      public:
      // void normal_func() final; // 编译报错,final不可修饰非虚函数
      virtual void func_3() const;
      };
      void Base_3::func_3() const {
      printf("Base_3 func_3");
      }


      class Derived_3 : public Base_3 {
      public:
      void func_3() const override final; // 注意顺序
      };
      void Derived_3::func_3() const { // 虚函数定义处不可使用override或final关键字,否则编译报错
      printf("Derived_3 func_3");
      }
  • 只存在派生类向基类的隐式转换,不存在基类向派生类的隐式转换。

    1
    2
    3
    4
    5
    6
    Bulk_quote bulk;
    Quote *itemP = &bulk; // 正确

    // 即使一个基类指针或引用绑定在一个派生类对象上,也不能执行从基类向派生类的转换
    // 编译器在编译阶段无法判断这种转换是否安全
    Bulk_quote *bulkP = itemP; // 错误
  • 虚函数与默认实参:实参值由本次调用的静态类型决定。(因此,若虚函数希望使用默认实参,则基类和派生类最好定义一致的默认实参。)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Base_3 {
    public:
    virtual int func_4(int, int);
    };
    int Base_3::func_4(int param_1 = 10, int param_2 = 20) {
    printf("Base_3 func_4\n");
    return param_1 + param_2;
    }


    class Derived_3 : public Base_3 {
    public:
    virtual int func_4(int, int) override;
    };
    int Derived_3::func_4(int param_1=30, int param_2=40) {
    printf("Derived_3 func_4\n");
    return param_1 + param_2;
    }

    int main(){
    Derived_3 derived_3;
    Base_3* base_3 = &derived_3;
    Derived_3* derived_3_temp = &derived_3;
    int res_1 = base_3->func_4(); // res_1 = 30 printf Derived_3 func_4
    int res_2 = derived_3_temp->func_4(); // res_2 = 70 printf Derived_3 func_4
    }
  • 回避调用虚函数:使用作用域运算符,强迫执行虚函数的特定版本

    1
    2
    double undiscounted_1 = basedP->Base::net_price(42);
    double undiscounted_2 = basedP->Derived::net_price(42);
  • 抽象基类

    负责定义接口,后续的其他类可以覆盖该接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Base_4 {
    public:
    int a = 10;
    virtual void func_pure_vir() = 0; // 纯虚函数, = 0 只能用在类内部虚函数声明处
    // void test() = 0; // 非虚函数当然不能定义为纯虚函数
    };
    /* void Base_4::func_pure_vir() { // 虽然语法没问题,但正常没人会为纯虚函数提供定义,而且该定义只能写在类的外部
    printf("Base_4 func_pure_vir");
    }*/


    class Derived_4 : public Base_4 {
    public:
    int b = 20;
    void func_pure_vir();
    };
    void Derived_4::func_pure_vir() {
    printf("Derived_4 func_pure_vir");
    }


    int main(){
    // Base_4 base_4; // 编译报错,含有(或未经覆盖直接继承)纯虚函数的类是抽象基类。抽象基类不可创建对象
    Derived_4 derived_4;

    return 0;
    }
  • 某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符;二是在派生类的派生列表中的访问说明符。后者对派生类的成员(及友元)能否访问其直接基类的成员没有影响,但对派生类的普通用户(使用派生类对象的用户)能否访问其直接基类的成员有影响。(详见C++ Primer p543/544)

  • 派生类向基类的转换

    派生类向基类的转换,受派生访问说明符影响。

    • 只有当D public继承B时,用户代码才能使用派生类向基类的转换。
    • 不论D以任何方式继承B,D的成员函数和友元都能使用派生类向基类的转换。
    • 如果D继承B的方式是public或protected,则D的派生类的成员和友元可以使用D向B的类型转换。(暂未遇到过这种情景,未做测试)

    关于类的普通用户的概念,详见C++ Primer p544/555。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    class Base_5 {
    private:
    int a = 10;
    protected:
    int b = 20;
    public:
    int c = 30;
    };


    class Derived_5: private Base_5 {
    public:
    void func();
    };
    void Derived_5::func() {
    Derived_5 temp;
    Base_5 *base_5 = &temp; // 派生类的成员函数或友元,都能使用派生类向基类的转换
    // int res_b = base_5->b; // 无论 class Derived_5: private/protected/public Base_5,都是编译报错
    int res_c = base_5->c; // 无论 class Derived_5: private/protected/public Base_5,都正确
    int res_c_2 = temp.c; // 无论 class Derived_5: private/protected/public Base_5,都正确
    int res_b_2 = temp.b; // 无论 class Derived_5: private/protected/public Base_5,都正确
    printf("2222");
    }


    int main(){
    Derived_5 derived_5;
    // Base_5 *base_5 = &derived_5; // class Derived_5: private/protected Base_5 编译报错,用户代码不允许对不可访问的基类"Base_5"进行转换
    derived_5.func();
    // derived_5.c; // 编译报错,普通用户不可访问private或protected成员
    return 0;
    }
  • 友元关系不可继承;每个类负责控制各自成员的访问权限。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Base_6 {
    private:
    int pri_mem = 10;
    protected:
    int pro_mem = 20;
    public:
    int pub_mem = 30;

    friend class Pal;
    };


    class Derived_6 : public Base_6 {
    protected:
    int j = 40;
    };


    class Pal {
    public:
    int f(Base_6 b) { return b.pri_mem; } // 正确,Pal是Base_6的友元
    // int f2(Derived_6 d) { return d.j; } // 错误,Pal不是Derived_6的友元
    int f3(Derived_6 d) { return d.pri_mem; } // 正确,Pal是Base_6的友元。每个类负责控制自己成员的访问权限,包括Base对象内嵌在其派生类对象中的情况
    };


    class D2 : public Pal {
    public:
    int mem(Base_6 b) { return b.pro_mem; } // 错误,友元关系不可继承
    }
  • 改变个别成员的可访问型

    using声明语句中名字的访问权限,由该名字自身的访问说明符来决定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Base_7 {
    public:
    std::size_t func_size() const { return n; }
    protected:
    std::size_t n;
    };


    class Derived_7 : private Base_7 {
    public:
    using Base_7::func_size; // 利用using,使得Base_7::size不再受private继承的影响,保持其原有的public属性
    protected:
    using Base_7::n; // 利用using,使得Base_7::n不再受private继承的影响,保持其原有的protected属性
    };
    // 改变之后,Derived_7的普通用户可以使用func_size成员,Derived_7的派生类能使用n。
  • 默认情况下,class派生类的派生列表中,访问说明符是private;struct派生类的派生列表中,访问说明符是public。

    1
    2
    class Derived : Base {...}	// 等价于class Derived : private Base {...}
    struct Derived : Base {...} // 等价于struct Derived : public Base {...}
  • 在编译时进行名字查找

    一个对象或引用或指针,的静态类型,决定了该对象的哪些成员是可见的,而非动态类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Base_8 {
    public:
    int a = 10;
    };

    class Derived_8 : public Base_8{
    public:
    int b = 20;
    };

    int main(){
    Derived_8 derived_8;
    Base_8 *base_8 = &derived_8;
    int temp_a = base_8->a;
    // int temp_b = base_8->b; // 编译报错,class Base_8 没有成员 b
    }
  • 名字冲突与继承

    派生类可以重用定义在其直接或间接基类的名字,进而隐藏直接或间接基类的内容。如若希望访问基类中因派生类重用名字而被隐藏的内容,可通过作用域运算符来访问。

    即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉

    但是,除了覆盖继承而来的虚函数外,派生类最好不要重用其他定义在基类中的名字。(C++ Primer p549)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    class Base {
    public:
    static void statmem();

    int func(int, int) const;
    };
    void Base::statmem() { printf("23333\n"); }
    int Base::func(int param1, int param2) const{
    int res = param1 + param2;
    return res;
    }


    class Derived : public Base {
    public:
    static void statmem(); // 重用名字

    int func() const; // 重用名字(即使形参列表不一致,也会对基类同名内容进行隐藏)

    void f(const Derived&);
    };
    void Derived::statmem() { printf("4444444444\n"); }
    int Derived::func() const {
    int res = 100;
    return res;
    }
    void Derived::f(const Derived &derived_obj) {
    Base::statmem(); // 23333
    Derived::statmem(); // 4444444444
    derived_obj.statmem(); // 4444444444
    this->statmem(); // 4444444444
    statmem(); // 4444444444 等价于 this->statmem();

    int res_1 = Base::func(10, 20); // 30 成员函数内,直接调用func可以,加作用域运算符调用也可以。作用域运算符不等价于静态成员
    int res_2 = Derived::func(); // 100
    int res_3 = derived_obj.func(); // 100
    int res_4 = this->func(); // 100
    int res_5 = func(); // 100 等价于 this->func();
    // int res_6 = func(10, 20); // 编译不通过,函数参数过多,因基类同名内容已被隐藏
    return;
    }
  • 虚函数与作用域

    ​ 基类与派生类中的虚函数必须有相同的形参列表。若两者接收的实参不同,则我们无法通过基类的指针或引用来调用派生类的虚函数。(不错的例子,详见C++ Primer p550)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    class Base {
    public:
    virtual int fcn();
    };

    class D1 : public Base {
    public:
    // 同名,隐藏基类的fcn,且此处的fcn不是虚函数。
    // 虽被隐藏,但D1仍旧继承了Base::fcn()的定义,如有必要可使用基类作用域运算符进行调用。
    int fcn(int); // 形参列表与Base中的fcn不一致
    virtual void f2(); // 一个新的虚函数,Base中不存在
    };

    class D2 : public D1 {
    public:
    int fcn(int); // 一个非虚函数,隐藏了D1::fcn(int)
    int fcn(); // 覆盖了Base的虚函数fcn
    void f2(); // 覆盖了D1的虚函数f2
    };

    Base bobj; D1 d1obj; D2 d2obj;
    Base *bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
    bp1->fcn(); // 虚调用,将在运行时调用Base::fcn
    bp2->fcn(); // 虚调用,将在运行时调用Base::fcn(D1直接继承了Base::fcn,没有进行覆盖)
    bp3->fcn(); // 虚调用,将在运行时调用D2::fcn

    D1 *d1p = &d1obj; D2 *d2p = &d2obj;
    bp2->f2(); // 错误,Base没有名为f2的成员(静态类型决定该对象哪些成员是可见的)
    d1p->f2(); // 虚调用,将在运行时调用D1::f2()
    d2p->f2(); // 虚调用,将在运行时调用D2::f2()

    Base *p1 = &d2obj; D1 *p2 = &d2obj; D2 *p3 = &d2obj;
    p1->fcn(42); // 错误,Base中没有接受一个int的fcn
    p2->fcn(42); // 静态绑定,调用D1::fcn(int) 由于我们调用的是非虚函数,所以不会发生动态绑定。实际调用的函数版本由指针的静态类型决定。
    p3->fcn(42); // 静态绑定,调用D2::fcn(int) 由于我们调用的是非虚函数,所以不会发生动态绑定。实际调用的函数版本由指针的静态类型决定。
  • 虚函数重载覆盖

    • 成员函数无论是否是虚函数,都能被重载。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    class Base_9 {
    public:
    // 虚函数与它的多个重载版本
    virtual void func(int);
    virtual void func(int, int);
    virtual void func(string);
    };
    void Base_9::func(int param_1) {
    printf("Base_9 func(int)\n");
    }
    void Base_9::func(int param_1, int param_2) {
    printf("Base_9 func(int, int)\n");
    }
    void Base_9::func(string param_1) {
    printf("Base_9 func(string)\n");
    }


    class Derived_9 : public Base_9 {
    public:
    /*
    1、覆盖基类虚函数 virtual void func(int);
    2、相对于基类Base_9而言,派生类Derived_9属内层作用域。在内层作用域中声明相同名字将隐藏外层作用域Base_9中的同名实体
    */
    void func(int) override;

    /*
    正常情况下,如果派生类希望某个虚函数所有的重载版本对于它来说都是可见的,则派生类要么覆盖该虚函数所有的重载版本(隐藏外层作用域中的同名实体),
    要么一个也不覆盖(不隐藏外层作用域中的同名实体)
    */
    // void func(int, int) override;
    // void func(string) override;
    };
    void Derived_9::func(int param_1) {
    printf("Derived_9 func(int)\n");
    }


    int main(){
    Derived_9 derived_9;
    derived_9.func(10);
    // derived_9.func(10, 20); // 编译报错,函数调用中参数过多。
    derived_9.Base_9::func(10, 20); // 正确,通过作用域运算符调用
    string str = "tempstr";
    // derived_9.func(str); // 编译报错,不存在从string到int的转换函数
    derived_9.Base_9::func(str); // 正确,通过作用域运算符调用
    }
    • 只想覆盖基类虚函数的一个重载版本,却不得不覆盖其所有的重载版本,过于繁琐。解决办法是使用using关键字,这样就无需覆盖基类中该虚函数所有的重载版本了。using声明语句指定一个名字而不指定形参列表

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      class Base_10 {
      public :
      virtual void func(int);
      virtual void func(int, int);
      virtual void func(string);
      };
      void Base_10::func(int param_1) {
      printf("Base_10 func(int)\n");
      }
      void Base_10::func(int param_1, int param_2) {
      printf("Base_10 func(int, int)\n");
      }
      void Base_10::func(string param_1) {
      printf("Base_10 func(string)\n");
      }


      class Derived_10 : public Base_10 {
      public:
      using Base_10::func;
      void func(int) override;
      };
      void Derived_10::func(int param_1) {
      printf("Derived_10 func(int)\n");
      }


      int main(){
      Derived_10 derived_10;
      derived_10.func(10); // Derived_10 func(int)
      derived_10.func(10, 20); // Base_10 func(int, int)
      string str = "tempstr";
      derived_10.func(str); // Base_10 func(string)
      }
  • 继承体系中的析构函数

    • 析构函数不能被继承,派生类如果需要,应自行声明析构函数。
    • 无需显式调用基类的析构函数,系统会自动隐式调用。(调用顺序与构造函数恰好相反:先执行派生类析构函数体,再执行基类析构函数体。 摘自链接
    • 基类应定义一个虚析构函数,用以动态绑定。(当delete一个动态分配的对象的指针时将执行析构函数,此时指针的静态类型与动态类型可能不一致。因此在基类中将析构函数定义为虚函数,确保执行正确的析构函数版本。)
    • 上文曾介绍过一条经验准则:即如果一个类需要自定义析构函数,那么它大概率也同样需自定义拷贝和赋值操作。但基类的析构函数不遵循该准则:基类自定义析构函数是为了将其声明为虚函数用于动态绑定,析构函数体完全可以为空,不能判断是否需自定义拷贝和赋值操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    class Base_11 {
    public:
    int *num;
    Base_11(int);
    virtual ~Base_11();
    };
    Base_11::Base_11(int val) : num(new int(val)) { printf("Base_11 construct.\n"); }
    Base_11::~Base_11() {
    printf("Base_11 destory.\n");
    delete num;
    num = nullptr;
    }


    class Derived_11 : public Base_11 {
    public:
    int *num_2;
    Derived_11(int, int);
    virtual ~Derived_11();
    };
    Derived_11::Derived_11(int val, int val_2) : Base_11(val), num_2(new int(val_2)) { printf("Derived_11 construct.\n"); }
    Derived_11::~Derived_11() {
    printf("Derived_11 destory.\n");
    delete num_2;
    num_2 = nullptr;
    }


    int main(){
    Derived_11 derived_11(10, 20);
    Base_11 *temp = &derived_11;
    return 0;
    }
    /*
    打印结果:
    Base_11 construct.
    Derived_11 construct.
    Derived_11 destory.
    Base_11 destory.
    */
  • 继承体系中的构造函数、析构函数、拷贝构造函数、拷贝赋值运算符,统一说明

    • 构造函数

      • 正常情况下,基类与派生类构造函数的调用顺序如上文所述。

      • 若派生类构造函数的构造函数初始化列表未显式调用基类构造函数,则派生类构造函数会隐式调用基类默认构造函数。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        class Base_11 {
        public:
        Base_11();
        };
        Base_11::Base_11() { printf("Base_11 construct.\n"); } // 与编译器生成的默认构造函数形式相同,这里自定义只是为了添加打印输出


        class Derived_11 : public Base_11 {
        public:
        Derived_11();
        };
        Derived_11::Derived_11() { printf("Derived_11 construct.\n"); }
        // 派生类构造函数会隐式调用基类默认构造函数,等价于Derived_11::Derived_11() : Base_11() { printf("Derived_11 construct.\n");

        int main(){
        Derived_11 derived_11;
        return 0;
        }

        /*
        打印结果:
        Base_11 construct.
        Derived_11 construct.
        */
      • 若派生类构造函数的构造函数初始化列表未显式调用基类构造函数,则派生类构造函数会隐式调用基类默认构造函数,此情况下,若基类无默认构造函数(基类显式自定义了任意一个构造函数,此时编译器不会再为你自动定义默认构造函数了,且该显式自定义的构造函数与默认构造函数形式不同),编译不通过。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        class Base_11 {
        public:
        Base_11(int);
        };
        Base_11::Base_11(int num) { printf("Base_11 construct.\n"); }


        class Derived_11 : public Base_11 {
        public:
        Derived_11();
        };
        Derived_11::Derived_11() { printf("Derived_11 construct.\n"); } // 编译不通过,报错:类“Base_11”不存在默认构造函数

        当然,你显式自定义出基类的默认构造函数就没问题了。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        class Base_11 {
        public:
        Base_11(); // 显式自定义出基类的默认构造函数
        Base_11(int);
        };
        Base_11::Base_11() { printf("Base_11 construct.\n"); }
        Base_11::Base_11(int num) { printf("Base_11(int num) construct.\n"); }


        class Derived_11 : public Base_11 {
        public:
        Derived_11();
        };
        Derived_11::Derived_11() { printf("Derived_11 construct.\n"); }


        int main(){
        Derived_11 derived_11;
        return 0;
        }

        /*
        打印结果:
        Base_11 construct.
        Derived_11 construct.
        */
    • 析构函数

      • 如上文所述
    • 拷贝构造函数

      • 拷贝构造函数的概念见上文。

        (注意:与合成默认构造函数不同,即使我们定义了其他构造函数,编译器也会为我们合成一个拷贝构造函数。)

        (拷贝构造函数也是一种构造函数,即显式自定义拷贝构造函数后,编译器不会再为你合成默认构造函数的。)

      • 对派生类进行拷贝构造时,如果想对基类成员同时进行拷贝,则应在派生类的拷贝构造函数的初始化列表中显式调用基类拷贝构造函数(也可在函数体内进行操作,但不规范,不建议)。若未在派生类的拷贝构造函数的初始化列表中显式调用基类拷贝构造函数,此时派生类的拷贝构造函数会调用基类的默认构造函数(注意,是默认构造函数,不是默认拷贝构造函数),不会对基类的成员变量进行拷贝,这样生成的对象,它的派生类部分和被拷贝对象的派生类部分一样,而基类部分则为默认构造函数的初始化结果。摘自链接

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      class Base_11 {
      public:
      int base_num;
      Base_11();
      };
      Base_11::Base_11() { printf("Base_11 construct.\n"); }


      class Derived_11 : public Base_11 {
      public:
      int derived_num;
      Derived_11(int);
      Derived_11(const Derived_11&);
      };
      Derived_11::Derived_11(int num) : derived_num(num) { printf("Derived_11 construct.\n"); }
      Derived_11::Derived_11(const Derived_11& derived_11) : derived_num(derived_11.derived_num) { printf("Derived_11 copy construct.\n"); }


      int main(){
      Derived_11 derived_11(20);
      Derived_11 temp = derived_11;
      return 0;
      }

      /*
      打印结果:
      Base_11 construct. 构建Derived_11对象
      Derived_11 construct. 构建Derived_11对象
      Base_11 construct. 这里派生类的拷贝构造函数调用了基类的默认构造函数
      Derived_11 copy construct.
      */
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      class Base_11 {
      public:
      int base_num;
      Base_11(const Base_11&); // 显式自定义基类拷贝构造函数(编译器不会再合成默认构造函数)
      };
      Base_11::Base_11(const Base_11 &base_11) : base_num(base_11.base_num){}


      class Derived_11 : public Base_11 {
      public:
      int derived_num;
      Derived_11(int);
      Derived_11(const Derived_11&);
      };
      Derived_11::Derived_11(int num) : derived_num(num) { printf("Derived_11 construct.\n"); } // 编译报错:类Base_1不存在默认构造函数
      Derived_11::Derived_11(const Derived_11& derived_11) : derived_num(derived_11.derived_num) { printf("Derived_11 copy construct.\n"); } // 编译报错:类Base_1不存在默认构造函数
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      class Base_11 {
      public:
      int base_num;
      Base_11();
      Base_11(const Base_11&);
      };
      Base_11::Base_11() { printf("Base_11 construct.\n"); }
      Base_11::Base_11(const Base_11 &base_11) : base_num(base_11.base_num) { printf("Base_11 copy construct.\n"); }


      class Derived_11 : public Base_11 {
      public:
      int derived_num;
      Derived_11(int);
      Derived_11(const Derived_11&);
      };
      Derived_11::Derived_11(int num) : derived_num(num) { printf("Derived_11 construct.\n"); }
      Derived_11::Derived_11(const Derived_11& derived_11) : Base_11(derived_11), derived_num(derived_11.derived_num) { printf("Derived_11 copy construct.\n"); } // 显式调用基类拷贝构造函数


      int main(){
      Derived_11 derived_11(20);
      Derived_11 temp = derived_11;
      return 0;
      }

      /*
      打印结果:
      Base_11 construct. 构建Derived_11对象
      Derived_11 construct. 构建Derived_11对象
      Base_11 copy construct.
      Derived_11 copy construct.
      */
    • 拷贝赋值运算符

      • 具体概念见上文。(注意,与拷贝构造函数一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。)

      • 与拷贝构造函数一样,派生类拷贝赋值运算符需显式调用基类拷贝赋值运算符

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        class Base_11 {
        public:
        int base_num;
        Base_11(int);
        Base_11& operator=(const Base_11&);
        };
        Base_11::Base_11(int param) : base_num(param) {}
        Base_11& Base_11::operator=(const Base_11 &base_11) {
        this->base_num = base_11.base_num;
        return *this;
        }


        class Derived_11 : public Base_11 {
        public:
        int derived_num;
        Derived_11(int, int);
        Derived_11& operator=(const Derived_11&);
        };
        Derived_11::Derived_11(int param_1, int param_2) : Base_11(param_1), derived_num(param_2) {}
        Derived_11& Derived_11::operator=(const Derived_11 &derived_11) {
        Base_11::operator=(derived_11); // 显式调用基类拷贝赋值运算符
        this->derived_num = derived_11.derived_num;
        return *this;
        }
  • 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数是被删除的或不可访问的,则派生类中对应的成员将也是被删除的。原因是编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值、销毁操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Base_11 {
    public:
    Base_11() {};
    Base_11(const Base_11&) = delete;
    Base_11& operator=(const Base_11&) = delete;
    };


    class Derived_11 : public Base_11 {
    };


    int main(){
    Derived_11 derived_11; // 正确
    // Derived_11 temp_1(derived_11); // 编译报错,拷贝构造函数已隐式删除
    // temp_1 = derived_11; // 编译报错,拷贝赋值运算符已隐式删除
    return 0;
    }
  • 容器与继承

    • 若容器元素为对象,则派生类部分将被“切掉”

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      class Base_11 {
      public:
      int base_num = 10;
      };


      class Derived_11 : public Base_11 {
      public:
      int derived_num = 20;
      };


      int main(){
      Base_11 base_11;
      Derived_11 derived_11;
      vector<Base_11> vec;
      vec.push_back(base_11);
      vec.push_back(derived_11);
      // int res = vec.back().derived_num; // 编译报错,class Base_11 没有成员 derived_num
      int res = vec.back().base_num; // 正确
      return 0;
      }
    • 为避免上述问题,可将容器元素类型定义为基类指针(智能指针更好),这些指针所指向对象的动态类型可为基类类型,也可为派生类类型。

  • 补充说明

    • 关于左值右值 参考链接
    • const成员函数:const关键字必须出现在函数声明和函数定义
    • override虚函数覆盖:override关键字只能出现在函数声明
    • final类或虚函数拒绝继承:final关键字只能出现在函数声明