Loading...
Navigation
Table of Contents

C++ Primer Plus NotesCode

notes

  • []++运算符优先级都比*高,所以*p++是先加再解除引用,int *p[5]是指针的数组
  • 数组名是不可知的表示(不能之间拿来赋值),所以int *p[5]后不能进行p=&a之类的赋值,但可以*p = a,因为p指向数组中第一个元素。
//如此类声明,会导致二义性嘛?
Stock(const char * s);
Stock& operator=(const char * s);
//main函数
Stock stock1 = "hi"; //这里的等号是隐式自动转换(初始化)
stock1 = "hello1"; //这里的等号是赋值运算符,调用operator=
  • 如果要判断一个指针是否为空,不要使用NULL == ptrnullptr == ptr这种写法,在不同的平台上可能有不同的结果。建议使用!ptr之类的写法,这种写法在多数平台上都没歧义。从此可以总结出,在不同的平台上,NULL可能会被定义为其他地址,当然这个地址都是非法不可访问的。此问题同样适用于C语言。
  • 空字符串""nullptr是不同的,空字符串创建了对象,只是里面没有内容,可用str.empty()(string对象)来判断,参考这里
  • 初始化和赋值的区别:
People p1;
People p2 = p1; //初始化
People p3;
p3 = p1; //赋值

2. 开始学习C++

  • cout << a*a里的<<*都是重载运算符
  • using namespace std可以放在文件头部,也可以放在同一个文件不同的函数里,也可以使用using std::cout这样的指令
  • sizeof运算符可求出变量占多少字节,参考这里
int a = 3;
char b[] = "123";
int *p = b;
sizeof (int);
sizeof a; // 4 int占4字节
sizeof b; // 4 当对象是数组时,返回数组总大小。注意字符串最后加一个\0
sizeof p; // 4 当对象是指针时,返回指针本身大小,注意这里和数组不同
sizeof (a + a); //4 当对象是表达式时,求的表达式返回值的类型大小,但并不计算表达式的值

3. 处理数据

变量命名 cost_of_trip或者costOfTrip

常量 对于整型常量,一般都将其看作int2018l则指示将其看作long常量,2018ul指示将其看作unsigned long常量; 对于浮点型,默认看成double2.018f则指示将其看作float常量

整型 win10下char占1字节(-128-127),short占2字节,int占4字节,long占4字节。对于宽字符,还有wchar_t类型,其输入输出应该用wcinwcout

浮点型 float占4字节,double占8字节

float a = 3.14E4; // 用E或e来表示指数
float a = 8.33e-4;

布尔型

bool is_ready = true;
bool is_ready = 10; //任何非0值都被隐式转换为true

const类型 const常量应该在声明的时候就进行赋值,如const int Months=12。在C++中应优先选择const而不是#define

不同进制

int a = 42 //10进制decimal
int b = 042 //8进制octal,10进制值为34
int c = 0x42 //16进制hexadecimal,10进制值为66
cout << hex //使用控制符修改cout输出格式

4. 复合类型

普通数组 只有定义数组时才能进行列表初始化,之后不能这样初始化了;也不能将一个数组赋值给另一个数组. 但是string可以相互赋值*

int cards[4] = {3, 4}; //列表初始化,后面2个元素会补0
double hand[] = {1.2, 2e-2, 0.33}; //自动计算元素个数
cout << cards; //0087DC,数组名其实就是第一个元素的地址

字符串 字符串需要以空字符结尾,int('\0')=0int('0')=48

char names[10] = {'a, b', '\0'}; //字符串数组,后面7个元素会补\0
char names[10] = "ab"; //和上一条等价
#include <cstring>
cout << strlen(names); //2 只显示可见字符

对于读取字符串到char a[10]中,有以下几种做法:

  • 正常读取cin >> a,首先清空缓冲区的分隔字符 (Enter、Space、Tab),然后开始读取,遇到分隔字符后结束,自动添加\0,并保留缓冲区尾部的分割字符。cin >>对于intdouble的读取是一样的
  • 读取一行cin.getline(a, 10),当碰到Enter或者读满9个字符时结束。缓冲区里的Enter字符丢掉。当getline()碰到数组填满了时候,会设置失效位,即cin.fail()==true,关闭后面的输入。更多参见第17章
  • 读取一行cin.get(a, 10),和getline类似,只是保留缓冲区里enter
  • 读取一个字符a=cin.get()cin.get(a)cin.get(),读取任意一个字符(到a中),注意cin.get()的参数列表是按引用传递,所以可以做到修改a的值
  • cin.get(),读取一个字符。对于cin.getline(a, 10),由于其会保留Enter符,所以后面的cin都将无法进行读。所以需要一个单独的操作读取走这个Enter符
  • 所以cin.get(a, 10).get()cin.getline(a, 10)是等价的,cin.get()存在的意义是判断停止读取的原因究竟是已经读完了整行,还是数组已经填满了

此外,应该注意到,对于cin.getline(a, 10)cin.get(a, 10),如果是因为读满了9个字符,则会标记失效位,即cin.fail()true,此时应该cin.clear()才能继续使用cin。更多详细内容参见CSDN

文件尾检测:检测到文件尾EOF之后,成员函数cin.fail()返回true,istream对象cin本身被bool转换后是false,以下演示了两种文件尾检测的方法 (p159)

char ch;
cin.get(ch); //先进行一次读取
//方法1 使用cin.fail()
while (cin.fail() == false) //没有到达文件尾则进入循环
    {...}
//方法2 使用cin
while (cin.get(ch)) //没有到达文件尾则进入循环

其他函数

strcpy(target, source); //复制char数组
strcat(target, source); //把source拼在target后面
strcmp(a, b); //比较a和b地址开始的字符串,相等时返回0,ASCII码中a<b时返回负数

string类

#include <string>
string name = "liu";
name[0] = 'a'; //可以用数组表示法访问string
string name2 = name; //开辟新内存
name.size(); //3 size是成员函数
cin >> name;
getline(cin, name); //历史遗留问题

指针 指针的值是地址,由整数表示。指针和数组名都是存放着地址,但是数组的地址不可改变。

int *a, *b; //声明两个指针
a = new int; //new运算符返回一个地址
b = new int[10];
b[1] = 12; //数组访问方法
*(b+1) = 12; //和上面等价
delete a; delete [] b; //删除指向的内存

对于指向结构的指针things *a = new things,既可以用句点运算符指定结构成员(*a).good,也可以用箭头成员运算符a->good


5. 循环和关系表达式

表达式1+1a=1都是表达式,其中后者称为赋值表达式,加上分号后就成了语句。表达式的值都是左边的值,如a=1是将1赋值给a后将a作为本表达式的值。

一个表达式可能被顺序点分割成为多个子表达式,这里顺序点是计算机程序中一些执行点,在该点处之前的求值的所有的副作用(对变量的修改)已经发生,在它之后的求值的所有副作用仍未开始. 顺序点包括: &&||,(int a,b不算,其左边必须有左操作数赋值)、三元运算符、函数调用等等。当具有子表达式的时候,表达式的值取最右边那个子表达式的值,如a = 1,b = a + 1式子的值为2,这是因为表达式的值是从左往右计算的。此外,逗号表达式的优先级最低,如a = 1,2被解释为(a = 1),2

对于顺序点之间(子表达式)中的多次副作用,其的发生顺序都是未定义的。如x=x++;x自增和赋值各一次,y = (1 + i++) + i++;里自增两次,C++不能保证这两个对x的副作用是谁先谁后,只能保证语句结束后进行两次+,参考这里这里

关系表达式:==运算符不可以用来比较两个字符串,而应该用strcmp()函数。但是如果比较中有一个是string对象,类函数重载使得可以使用==比较,如string a = "mate"; a == "mate";

对于字符对比,可以直接用,如a == 'a'

类型别名 有时候想为某些类型添加一些别名,推荐typedef

typedef (int*) pINT; //使用typedef关键字
#define pINT2 int* //使用"预处理器",没有分号
pINT a, b; //int* a; int* b;
pINT2 a, b; //int* a, b; 这里b不是指针

然而在C++ 11中,更推荐用可读性更好的`using`,而不是`typedef`,见这里

for循环(c++11)

int princes[] = {1,2,3,4};
for (int i : prices)
    cout << i << endl;
for (int &i : prices)
    i += 2; //使用引用才可以修改数组

二维数组 指针数组与数组指针,参考这里

int pa[4][5]; //数组指针,共4个元素,其中单位元素为int[5]
void process(int (*pa)[5]) //将以上二维数组作为参数的函数
//这里pa是一个指针,指向的单位元素为int[5],和上面一致
pa++; //pa指向下一组int[5]

char* arr[4]; //指针数组,这里arr是一个数组名,其单位元素为指针char*
arr[i] = new char[4]; //每个arr[i]都是指针

6. 分支语句和逻辑运算符

优先级 &&||运算符 < 关系运算符 < !运算符。


7. 函数

数组与指针 多数情况下,数组名和指针是相同的,然而也有一些不同点,参考这里

  • 数组名只有在sizeof&操作符后为指向整个数组的指针(如,类型为int[n]),其余情况都是看成指向数组首元素的指针(类型为int),参考这里
  • &作用于数组名,得到的是整个数组占的内存块的地址; 而数组名本身是第一个元素的地址
  • 对于函数声明,int sum(int arr[], int n)int sum(int *arr, int n)都是把arr看成指针的!但是对于用户,arr[]表示其是指向一个数组,而*arr表示其指向单个数
int a[3] = {1, 2, 3};
cout << a << &a; //都是a[0]的地址,但是类型应该不同
char b[3]={'h','a','\0'};
cout << b << &b; //前者输出字符串,后者输出地址; 这是由cout的特性决定的
int (*p1)[3] = &a; //有效,p1指向int[3]
int (*p2)[3] = a; //无效,左边指向int[3],右边是个int的地址(a[0]的地址)

const指针 int* const a表示a指向的内容不可修改,但是a本身可以修改;const int* a表示a本身不可以修改,但是可以指向不同的内存区域,更多参考这里

函数指针:TBD


8. 函数探幽

引用 &除了作为地址运算符,还可以用来声明引用:int & a = b;。引用必须在声明语句中就被赋值,其本值就是一个变量的别名,之后不可再修改其引用的对象

int a = 10, c = 20;
int & b = a;
cout << &b; //address 1
b = c; //这里的意思是把a和b的值都赋值为20
cout << &b; //依然是address 1
  • 函数的形参是引用的话,实参则不能是表达式
  • 引用尽可能加const,这在参数不匹配时会自动创建临时变量(此时引用则会失效,保证原数据没被修改)
long a = 1;
ref_swap(a + 1); //错误
ref_swap(const int& a); //会创建临时变量
  • 引用主要被设计用于结构体而不是基本变量的
  • 函数返回的引用,应避免返回对临时变量的引用

函数重载 重载的关键在于参数列表(特征标,function signature)不同,且对于一组实参可以进行顺利匹配。反例: int aint& a会引起匹配失败

函数模板 函数模板的声明写在具体函数的前面,也要写在函数声明的前面。第三代显示具体化模板*

template <typename T>
void swap(T& a, T& b);

9. 内存模型和名称空间

头文件 头文件中一般包含函数原型,使用#defineconst定义的常量,结构声明,类声明,模板声明,内联函数。<head.h>表示从存储标准头文件的位置查找,"head.h"表示优先从当前工作目录查找。

此外,对于#include<iostream>这样的无后缀表达,C++ 并没有要求存在名为iostream的文件,这只是编译器在具体实现时取巧的结果,参考知乎

//head.h头文件示例
//取一个对应这个文件名的名字,防止头文件被重复include后重复声明
#ifndef head_h_ //如果没有head_h_
#def head_h_ //那么定义head_h_
... //并进行程序段1
#else
... //否则进行程序段2
#endif

存储持续性、作用域和链接性

  • 在多文件编程中,C++的变量存储的生命周期有三种类型:自动存储持续性(函数内定义的变量),静态存储持续性(函数外定义的变量,用static定义的变量),动态存储持续性(用new分配的内存);
  • 作用域scope描述了变量名称在当前文件(翻译单元)中都多大范围内可见,作用域为局部表示仅在代码块中可见,为全局则表示从当前声明位置到文件尾都可见;
  • 链接性linkage描述了变量名词如何在不同单元之间共享:链接性为外部则可以在文件间共享,为内部则仅在当前文件共享。

默认情况下,函数内声明的变量为自动变量,作用域为局部,无链接性,变量存储在单独的栈中; 静态变量存储在单独的内存块中(不需要使用栈来管理他们),未被初始化的所有位都被置为0,并且只被初始化一次。

int global = 10; //静态变量,链接性外部
static int this_file = 1; //静态变量,链接性内部
const int a = 1; //链接性内部,和static const int a = 1;相同
void func(){
    static int count = 0; //静态变量,无链接性
    int tmp = 1; //自动变量
}

注意到,在函数内的静态变量只被初始化一次,并且无链接性。这意味着该变量在代码块不活跃时依然存在,在两次调用函数之间,静态局部变量的值保持不变。

单定义规则:对于外部变量,如果想在另一个文件使用,则需要用extern关键字重新声明;如果想在另一个文件中临时覆盖它,则使用static声明内部静态变量。externstatic同样适用于函数声明,默认情况下函数的链接性为外部。

名称空间 名称空间保证了不同空间的变量和函数不会冲突,C++中带有一个全局名称空间,可以使用using声明或者using编译指令来调用某个名称空间的名称。

namespace Jill{
    int var = 1;
    double bucket(double n);//函数原型
}
int var = 0;
using Jill::var; //using声明,这样会和全局名称空间里的var冲突
using namespace Jill; //using编译指令,这样不会冲突,但是本地var会隐藏Jill::var
std::cout << var; //输出0
std::cout << Jill::var; //输出1

10. 对象和类

类由类声明和类方法定义组成。在类声明中,数据/成员函数默认访问类型是private(这点和struct不同)。在类声明中也可以声明函数体,这样则是作为内联函数。

//类声明文件stock00.h
#ifndef STOCK00_H_
#define STOCK00_H_
class Stock{
private: //可不写
    std:string company;
    double total_val;
    void set_tot() {total_val += 1;}
public:
    Stock(); /默认构造函数
    Stock(const string & c_name, double val); //构造函数
    ~Stock(); //析构函数
    void buy(long num, double price);
    void show() const; //const成员函数,适用于const的类对象
}; //记得加分号
#endif //不要忘记

//类方法文件stock00.cpp
Stock::Stock(){ //默认构造函数
  company = "noname";
  total_val = 0;
}
Stock::Stock(const string & c_name, double val){ //普通的类构造函数
    company = c_name;
    total_val = val;
}
Stock::~Stock(){ //析构函数
    std:cout << "bye";
}

//use_stock.cpp
Stock stock1("name1", 1); //构造对象方法1
Stock stock2 = Stock("name2", 2); //构造对象方法2
stock1 = Stock("tmpname", 3); //构造临时对象,并赋值给stock1 (覆盖那片内存)
// 此时临时变量Stock("tmpname", 3)会被析构
Stock stock3 {"name3", 3}; //C++ 11 列表初始化

构造/析构函数:构造函数无声明类型,默认构造函数无声明类型和参数。提供了构造函数,就必须提供默认构造函数;析构函数没有参数,也可以没有声明类型

对象数组:书上说,在声明对象数组时Stock a[5]={Stock("name1",1)};,先用默认构造函数生成5个对象,然后用参数列表生成5个临时对象,再进行赋值。但是在vs2017中并不是如此,而是直接生成5个对象。

静态成员 静态数据/函数在类中是共享的,不依赖于具体对象,参考这里

初始化方法:具体参考常量数据成员,静态数据成员,常量静态数据成员的初始化方法

  • const成员和引用成员:只能在构造函数后的初始化列表中初始化
  • static成员:初始化在类外,且不加static修饰
  • const static 整型成员:此成员只有唯一一份拷贝,且数值不能改变,且是整型. 只有这样才可以在类中声明处初始化,也可以像static在类外初始化

静态成员函数:不能通过对象调用静态成员函数;如果静态成员函数是在公有部分声明的,可以通过类名来访问它,更多参考菜鸟教程

  • 静态成员函数没有this指针,只能访问静态成员(包括静态成员变量和静态成员函数)
  • 普通成员函数有this指针,可以访问类中的任意成员; 而静态成员函数没有this指针
class Year{
private:
    static int Months; //静态成员变量
    const int Months2; //常量成员
    Year & year1; //引用成员
    static const int Months3 = 12; //静态成员常量
    double a[Months3];
public:
    Year(int x, Year & y):Months2(x), year1(y){//初始化列表初始化
        std::cout << "Complete\n";
    }
    static int set_months(int x){Months = x;} //静态成员函数只能访问静态成员常量
};

int Year::Months = 12; //在类外初始化静态成员数据
void main(int argc, char *argv[]){
    Year::set_months(5); //在类外访问静态成员函数
}

11. 使用类

重载运算符 例如,若重载了Time类的+运算符,即Time operator+(const Time & t) const;,对于两个对象相加a+b,则左边是被调用的对象,右边是参数,其等价于a.operator+(b)

友元函数 在为类重载二元运算符时,常用到友元函数。友元函数不是类的成员函数,但拥有和成员函数相同的访问权限。注意这里只是权限,具体访问还是得用Object.Var或者Object::StaticVar这种不省略对象名称的形式。以下演示重载二元运算符和一元运算符,友元函数的例子

using std:ostream;
class Time{
    int hours;
public:
    Time operator+(const Time & t) const; //普通重载+运算符
    Time operator-() const; //重载取反运算符-,而不是减号运算符
    //开始用友元函数重载二元运算符<<,实现cout << time;的功能
    friend ostream & operator<<(ostream & os, const Time & t); //在类声明中加friend关键字
    friend op
}

Time Time::operator+(const Time & t) const{ //这是成员函数
    Time sum;
    sum.hours = hours + t.hours;
    return sum;
}

Time Time::operator-() const //一元取反运算符
    return Time(-hours);

ostream & operator<<(ostream & os, const Time & t){
//友元函数实现中不加friend,也不加Time::
    os << t.hours << endl;
    return os;
}

类的自动转换 如果类的构造函数只有一个参数,其可实现自动(显式/隐式)类型转换的功能。这里是指从基本类型,如int,转化成类对象。

//类构造函数声明,这两种方式都可以
Time(double a); //只有一个参数
Time(double a, int b = 0); //或者有两个参数,第二个有默认值

//使用自动转换
Time t = 19.6; //隐式自动转换,先构造一个临时对象,再赋值给t
Time t = Time(19.6); //显示自动转换方法1
Time t = (Time) 19; //显示自动转换方法2,这里先将19转化为double再转化为Time

//以下演示了一个隐式自动转换的例子
void show_time(const & Time a) {std::cout << a;} //声明一个函数
show_time(19.6); //这里将先隐式地将19.6转化为Time对象,然后传入show_time中

//有时候为了防止出错,不想进行隐式的自动转换?
explicit Time(double a); //使用explicit关键字来表示这个构造函数不支持隐式自动转换

强制类型转换 有时候想把类对象强制(显式/隐式)地转换为基本类型,如int,那就需要重载int的运算符

//类声明
Time::operator int() const;
explicit Time::operator double() const; //只允许显示强制转换

//使用强制类型转换
Time t(19.6); //声明一个对象
int a = t; //隐式强制转换
double b = double(t); //显式强制转换

12. 类和动态内存分配

特殊成员函数 对于复制构造函数,赋值运算符,如果不显示初始化,那么编译器将生成默认的版本。当进行类对象复制/拷贝(copy)时,如果私有变量里有指针,则会发生奇怪的错误,因为默认版本的赋值只会把地址复制过去(浅复制),而忘记用new申请新内存然后进行深度复制(deep copy)。记住何时会使用复制构造函数和赋值运算符是非常重要。

String s("helloo");
String a = s; //Case1 新建并初始化. 会调用复制构造函数
String a = "hello"; //新建并初始化. 还记得嘛? 这里会使用隐式自动转换
func(s); //Case2 函数按值传递(不是按引用). 会使用复制构造函数
s + s + s; //Case3 需要生成临时对象. 会调用复制构造函数
String a; a = t; //这里会调用赋值运算符

String::String(const String & s); //需要手动定义复制构造函数以解决潜在问题
String & String::operator=(const String & s){//需要手动重载赋值运算符以解决潜在问题
    if (this == &s) //关键点1. 避免给自身
        return *this; //关键点2. 应返回自身引用,使得 a = b = c这种语句可用
    delete [] str;
    str = new char[st.len + 1]; //先删后申请内存是个不好的做法. 详见剑指offer第一节
    std::strcpy(str, st.str);
    return *this; //关键点2.
}

13. 类继承

从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。

// tabtenn1.h -- a table-tennis base class
class TableTennisPlayer { //基类
private:
    string firstname;
    string lastname;
    bool hasTable;
public:
    TableTennisPlayer (const string & fn = "none",
                       const string & ln = "none", bool ht = false);
    void Name() const;
    bool HasTable() const { return hasTable; };
    void ResetTable(bool v) { hasTable = v; };
};

class RatedPlayer : public TableTennisPlayer { //公有派生
private:
    unsigned int rating; //派生类额外的数据成员
public:
    //派生类自己的构造函数
    RatedPlayer (unsigned int r = 0, const string & fn = "none",
                 const string & ln = "none", bool ht = false);
    //派生类自己的构造函数2
    RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
    unsigned int Rating() const { return rating; }
    //派生类额外的成员函数
    void ResetRating (unsigned int r) {rating = r;}
};
  • 派生类存储了基类的数据成员和方法,但是不能直接访问基类的私有成员/方法(private),而必须通过基类方法进访问;
  • 在创建派生类对象时,程序会首先通过基类构造函数创建基类对象,然后使用派生类对象构建派生类对象,这意味着派生类构造函数里面必须使用成员初始化列表的写法来说明使用哪一个基类构造函数来构造对象。
//1. 将调用TableTennisPlayer的默认构造函数
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) {
    rating = r;
}
//2. 和1中的例子完全相同
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & l, bool ht) : TableTennisPlayer() {
    rating = r;
}
//3. 将调用TableTennisPlayer(fn, ln, ht)构造函数
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
     const string & ln, bool ht) : TableTennisPlayer(f, ln, ht) {
    rating = r;
}
//4. 另一种构造函数
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
    : TableTennisPlayer(tp) {
    rating = r;
}

可以用派生类对象初始化基类对象,即使是基类中没有对应的构造函数

RatedPlayer p(1840, "a", "b", true);
TableTennisPlayer p1(p); //因为存在隐式复制构造函数
TableTennisPlayer p2;
p2 = p; //因为存在隐式重载赋值运算符

is-a关系 一般来说,存在如下关系:

  • is-a关系:香蕉是水果;
  • has-a关系:午餐中有水果,但午餐不是水果;
  • is-like-a关系:律师像鲨鱼,但是律师不是鲨鱼;
  • is-implemented-as-a关系:可以用数组实现栈,但从Array类派生出Stack类是不合适的;
  • uses-a关系:计算机可以使用打印机,但是Computer类派生出Printer类是没意义的;

在C++中,可以用公有继承建立has-a、is-implemented-as-a和uses-a关系。不够只建议使用is-a关系,这最符合类继承的定义。

基类/派生类之间的关系

  • 派生类可以使用基类的公有(public)成员以及保护(protected)成员,不能访问基类的私有(private)成员,外部的其他类连保护成员都无法访问;
  • Up casting:对派生类的指针/引用可以转化为指向基类,is-a关系保证了隐式类型转换是合法的;
  • Down casting:对基类的指针/引用不可以转化为指向派生类,因为调用派生类的方法会出错;但是也可以通过显示强制类型转换来实现;
  • 静态联编:在没有使用虚方法的前提下,通过基类指针/引用调用方法时(不是通过对象本身来调用),程序是根据指针/引用本身的类型来判断是选择基类的方法还是派生类的方法;
  • 多态和动态联编:当通过指针或引用来调用对象的虚方法时,程序会根据指针/引用指向的对象的类型,来判断是选择基类的方法还是派生类的方法,这种现象叫动态联编;
  • 参考C++基类和派生类指针的相互赋值和转换

虚方法 加关键字virtual的方法是虚方法

  • 构造函数不应该是虚方法;而析构函数应该是虚方法,除非这个类不会用作基类;
  • 友元函数不是虚方法,因为友元不是类成员,它只是和类成员有相同的权限;
  • 如果派生类中没有进行继承,则使用该函数的最新基类版本;如果派生类是处于多级派生链中,则使用最新的虚函数版本;
  • 在派生类中继承方法时,虽然方法名称和参数列表都必须相同,但是返回值可以不同。例如,若基类中返回的是基类的指针,那么派生类中可以相应的修改成派生类指针,这种特性被称为返回类型协变(covariance of return type);
  • 如果基类中含有某方法的多个版本(方法名称相同,但是参数列表不同),派生类应该对每个版本的方法都继承;没有被继承的版本将被隐藏,派生类对象将无法访问这种方法,见如下代码:
class Base {
public:
    virtual int a();
    virtual int b();
    virtual int c();
    virtual int c(int a);
};
class Derived: public Base {
public:
    virtual double a(); //返回类型协变
    virtual int c();
};
Base* p = new Derived(); //基类指针指向派生类对象
p->a(); //调用Derived->a(),这是正常的多态现象
p->b(); //调用Base->a(),如果派生类没有进行继承,则调用基类的版本
p-c(996); //报错,因为没有将基类所有版本的a()都继承,因此基类中的c(int a)方法被隐藏

抽象基类/纯虚函数 包含至少一个纯虚函数的类是抽象基类(Abstract Base Class, ABC),这种类不能创建实例对象,其存在的意义是提供了一个接口(类似于Java里的接口),从而迫使派生类覆盖其纯虚函数。

class AcctABC {
public:
    AcctABC(string & s);
    virtual void Withdraw(double amt) = 0; //纯虚函数
    virtual ~AcctABC() {}
}

继承和动态内存分配 在使用动态内存分配的时候,主要要考虑复制构造函数、赋值运算符、析构函数这几个函数的设计。如果派生类中没有使用new来申请动态内存,这几个函数都不需要显示重写(使用编译器构造的默认版本即可)。比如,派生类在析构时,会自动调用基类的析构函数释放内存(使用string类存储字符串时就是这样)。

然而,当在派生类中使用new时,必须显示地定义复制构造函数、赋值运算符、析构函数:

//base class
class People {
private:
    char* name; //每个人都有个名字
public:
    People(const char* n = "Small Liu"); //普通构造函数
    People(const People& p); //复制构造函数
    People& operator=(const People& p); //赋值运算符
    friend std::ostream& operator<<(std::ostream& os, const People& p); //友元函数
    virtual ~People(); //析构函数
}

People::People(const People& p) {
    name = new char[std::strlen(p.name) + 1];
    std::strcpy(name, p.name);
    
}

People& People::operator=(const People& p) {
    if (this == &p)
        return *this;
    delete [] name;
    name = new char[std::strlen(p.name) + 1];
    std::strcpy(name, p.name);
    return *this;
}

std::ostream& operator<<(std::ostream& os, const People& p) {
    os << "name: " << p.name << std::endl; /打印人的名字
    return os;
}

People::~People() { delete [] name; }
//derived class with People
class Programmer: public People {
private:
    char* tshirt_color; //程序猿都有特定颜色的格子衫
public:
    Programmer(const char* n = "Small Liu", const char* c = "green"); //默认是绿色的
    Programmer(const Programmer& p); //复制构造函数
    Programmer& operator=(const Programmer& p); //赋值运算符
    friend std::ostream& operator<<(std::ostream& os, const Programmer& p); //友元函数
    virtual ~Programmer(); //析构函数
}

Programmer::Programmer(const Programmer& p) : people(p) {
    //在复制构造函数中调用基类的复制构造函数
    t_shirt_color = new char[std::strlen(p.tshirt_color) + 1];
    std:strcpy(tshirt_color, p.tshirt_color); //拷贝格子衫的颜色
}

Programmer& Programmer::operator=(const Programmer& p) {
    if (this == &p)
        return *this;
    People::operator=(p); //调用基类的赋值运算符
    // *this = p; 这种写法也可以
    delete [] tshirt_color;
    tshirt_color = new char[std::strlen(p.tshirt_color) + 1];
    std::strcpy(tshirt_color, p.tshirt_color); //拷贝格子衫的颜色
    return *this;
}

std::ostream& operator<<(std::ostream& os, const Programmer& p) {
    os << (const People&) p; //先用基类的友元函数打印人名字
    //因为友元不是基类的成员函数,因此无法用解析运算符指出用哪个函数,所以只能强制类型转换
    os << "T-Shirt color: " << p.tshirt_color << std::endl; /打印格子衫的颜色
    return os;
}

Programmer::~Programmer() { delete [] tshirt_color; } //删除格子衫的颜色

Last updated on Jun 18, 2019.