欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++ Primer Plus笔记

程序员文章站 2023-12-22 21:44:22
...

Chap 7. Function.

为什么需要原型?

  1. 为了编译器的效率:如果不事先列出原型,直接在整个代码文件内查找,编译的效率将受到影响。
  2. 多个程序的组合,受到文件访问权限的影响。

example:

void CheckValidPath(int number, vector <int> path);

术语:

  • argument 实参 在函数外部使用,并作为函数输入的参数
  • parameter 形参 在函数中使用的参数

函数和数组

int sum_arr(int arr[], int n);
// arr实际上为指针,并输入元素个数n。只有函数原型能这么处理。
int sum_arr(int* arr, int n);
int array[5];

array+1 等价于 &array[1]

指定数组起始位置及元素个数

void show_array(const int arr[], int n);

指定数组元素区间

void show_array(const int* begin, const int* end);

指针和const

int age = 20;
const int * pt = &age; // 指向const的指针
*pt = 30; // invalid
cin >> *pt; // invalid
age = 30; // valid

指向const的指针,其指向变量的内容不可修改;但变量自己能够修改。

const float data1 = 3;
const float * p1 = &data1; // valid
const float data2 = 4;
float * p2 = &data2; // invalid 防止通过指针修改数据内容

int data1 = 3, data2 = 4;
const int * p1 = &data1; // a pointer to const int: (const int)
p1 = &data2; // valid
int * const p2 = &data1; // a const pointer to int: (int *)
p2 = &data2; // invalid
(*p2)++;     // valid 该指针虽然不能改变指向的地址,但可以修改固定地址内数据!
const int* const p3;     // 这种定义是最安全的。既不能改变指向的地址,也不能改变指向地址内的数据。

函数与二维数组

调用方式

int data[3][4];
int total = sum(data, 3);

正确的原型

int sum(int (*arr2)[4], int size);
int sum(int arr2[][4], int size);

arr2指向数组(元素为由4个int组成的数组)

arr2[r][c] == *(*(arr2 + r) +c);
*(arr2 + r) +c == &arr2[r][c];

函数与C风格字符串

C风格的字符串与char数组的区别是,字符串包含空值字符'\0'。

unsigned int charNum(const char * str, char ch);

函数与结构

避免结构形参复制时间的方法:使用结构指针
函数原型:

void fill(std::array <double, 4> * parr);

具体调用:

std::array<double, 4> money;
fill(&money);

递归

  • 分而治之策略(divide-and-conquer strategy)

例如二分查找、快速排序算法就是典型的分而治之策略

void subdivide(int arr[], int low, int high){
    // conquer strategies
    ....
    // divide
    int mid = (low + high)/2;
    subdivide(arr[], low, mid);
    subdivide(arr[], mid, high);
}

函数指针

获取函数地址的方法:

process(think);   // passes address of think() to process()
thought(think()); // passes the return value of think() to thought()

声明函数指针的方法:

double process(int num);
double (*pf) (int); // pf为一个函数指针,指向带一个int参数,且返回double类型的函数: 运算符() 的优先级比 * 高
double * pf (int);  // pf为一个函数,输入为int参数,且返回变量为指向double变量类型的指针。

使用函数指针的方法:

pf = process; // 注意,函数名不加括号
int num = 0;
double d1 = process(num);
double d2 = (*pf)(num);   // 使用函数指针pf调用process
double d3 = pf(num);      // 使用函数指针pf调用process的另一种方法

两种函数指针的调用方法都是等价的。第一种方法更强调其指针的属性,第二种则强调函数“别名”的特点

调用函数指针:

void estimate(int num, double (*pf)(int));    // 函数声明
void estimate(int num, double (*pf)(int)){    // 函数定义
    cout << pf(num) << endl;
}

函数指针数组:

double (*pf[3])(int) = {f1, f2, f3}; // 运算符[]的优先级高于*,pf为包括三个指针的数组。

因此,使用auto能把精力从细节抽出,主要放在程序设计上。
或者使用typedef:

typedef const double* (*p_func)(const double*, int);
p_func p1 = f1;
p_func pa[3] = {f1, f2, f3};    // pa为三个函数指针的数组,每个数组的元素指向类型一样的不同函数
p_func (*pd)[3] = &pa;           // 指针,指向函数指针数组

Chap 8. 函数探幽

C++ 内联函数

普通的函数需要在内存中开辟一块临时空间并跳转至函数内部进行操作。
内联函数相当于把整个函数程序复制到调用的地方,不经过跳转,但代价是占用更多的复制空间。一般inline函数不写原型,直接写定义。最好是较短的函数。

inline double square(double radius){
    return radius*radius;
}

用C的宏实现只是相当于文本替换,应该考虑转换为内联函数。
注意:如果一个inline函数会在多个源文件中被调用,那么必须把它定义在头文件中。因为inline告诉编译器,编译器直接将对应定义部分拷贝至调用处;而头文件中若不含有定义,则无法解析。

引用变量

int num1 = 0, num2 = 3;
int& data = num1; // 使用引用必须在声明引用时就初始化。
data = num2;      // data作为num1的别名,被赋值,此时num1 = num2, num1 == 3

因而,函数声明与定义也要考虑使用引用变量后,是按值传递,还是按引用传递。

int a = 0, b = 3;
void swapr(int &a, int &b);    // 调用该函数后函数外部的两个变量值交换
     swapr(a, b);
void swapp(int *a, int *b);    // 同上
     swapp(&a, &b);
void swap(int a, int b);       // 只在函数内部有交换,退出临时空间后不作用于外部

保护按引用传递的变量写法如下:

double refcube(const double& refrad);

一个有趣的现象:

double rad = 2.0;
cout << refcube(rad) << ": " << rad << endl;
cout << rad << endl;

结果为

8.0 2.0
8.0

因而,尽可能将引用形参声明为const,因为:

  • 可避免函数无意中修改数据的错误
  • 将const变量与非const数据分开
  • 使用const引用能够让函数正确生成并使用临时变量

如:

struct player{
    std::string name;
    int success;
    int times;
    float rate;
};
player& accumulate(player& target, const player& source){
    target.success += source.success;
    target.times += source.times;
    target.rates = 100.0f * float(success) / float(times);
    return target;
}
player result = accumulate(total, player0);

此时返回值为指向结构的引用,直接将改动后的total结果复制到result,避免了复制一次整个结构作为临时变量的内存开支。

返回临时变量的调用将会导致内存崩溃。如:

const string& combine(string& s1, const string& s2){
    string temp;
    temp = s2 + s1 + s2;
    return temp;
}

在外部调用此函数,返回结果随着函数退出,临时变量temp的空间也将被释放。外部试图访问不存在的变量空间,造成内存泄漏。

默认参数

函数原型写法:

char* subpart(const char* str, int num = 1);

函数多态

examples:

void print(const char* str, int length);
void print(double data, int length);

而返回类型不同时,特征标志(signatures)也必须不同。如:

long process(int a, float b); // valid 1
double process(float a, float b); // valid 2
double process(int a, float b); // 3 corrupt with 1; 3 with 2 is ok.

函数模板

需要多个对不同类型使用同一种算法的函数,模板是较好的选择。

declaration

template<typename T> // template <class T> is ok as well. template, typename, class are keywords.
void change(T& a, T& b);

definition

template<typename T>
void change(T& a, T& b){
    T& temp;
    temp = a;
    a = b;
    b = temp;
}

模板是具有局限性的。比如,当输入为数组、结构时,运算符 > 和 < 便不成立;此时,可以进行运算符重载,或者为特定类型提供具体化(specialization)的模板定义。

  • 显示具体化(explicit specialization)
    如下列原型:

    template<> void change(person& a, person& b);
    template<> void change(int& a, int& b);

  • 实例化(instantiation),分为隐式(implicit)和显式(explicit)

    template void change(int& a, int& b);

显式实例化:根据显式的声明,直接使用模板生成特定类型的实例函数。
显示具体化:不使用模板的函数定义,针对这种数据类型,使用特定的具体函数。

template<> void Swap<person>(person &, person &); // 显式具体化
template void Swap<char>(char &, char &);    // 显式实例化的作用是什么?
  • 重载解析
    整数类型不能被隐式地转换为指针类型。

  • 排序规则
  1. 指定的特征标志是否为指针?输入参数的类型将会自动匹配至对应的模板。
  2. 是否有显式实例化的函数?在调用对应函数后面使用模板符号<>,将优先调用模板函数而不是返回具体类型的函数。
  • 模板自动指定返回类型
    example:

    template <class T1, class T2> auto add(T1& a, T2& b) -> decltype(a+b){
    decltype(x+y) temp = x+y;
    return temp;
    }

Chap 4. 复合类型

数组名是指向整个类型的指针,其大小是整个数组,例如:

int arr[5] = {1, 2, 3, 4, 5};
cout << sizeof(arr) << sizeof(arr[0]) << sizeof((int*) arr) << endl;

分别输出 20, 4, 4。

数组初始化

example:

int arr[3] = {};       // 所有元素初始化为0
int arr[3] = {1, 2};   // 余下的元素初始化为0

列表初始化禁止缩窄转换,如

int example[] = {1, 2, 1.5};     // 将浮点数转换为整型为缩窄转换

字符串数组

承接数组初始化的特性,余下的元素初始化为0。不是字符'0'

char temp = 's';    // valid
char temp = "s";    // invalid: the string elements including 's' and '\0'.
char temp[] = "s";  // valid

字符串常量的拼接

char str[] = "I love"
             "C++";

两个字符串常量将会拼接为一个,且第一个字符串的'\0'将会被第二个字符串的首元素取代。

strlen v.s. sizeof

char str[8] = "12345";
cout << strlen(str) << sizeof(str) << endl;

分别为5和8.strlen计算可见的字符,sizeof计算数组的大小。

字符串输入的缺陷
cin的检测填充机制

char str1[20];
char str2[20];
cin >> str1 >> str2;
cout << str1 << ": " << str2 << ".\n";

若str1有空格,则自动将后半部分的字符串填充至cin的下一个对象str2,而不再等待输入。

改进cin方法的函数有两种:

cin.getline(str1, N);
cin.get(str2, N).get();
  • string风格字符串

    string str1, str2;

cin时会将空格键作为分割字符串的标识符。因而

cin >> str1;
cin >> str2;
cout << str1+str2;

输入"I try to ", "learn C++",输出结果为"Itry",str1 = "I", str2="try".
若改进为

getline(cin, str1);
getline(cin, str2);
cout << str1+str2;

输出为"I try to learn C++"。此处getline()不是istream的类方法,而是其友元函数。

枚举类型

example

enum spectrum = {red, orange, yellow, green, blue};
spectrum band;
band = 3; // invalid, not able to convert integer type to spectrum type
band = green; // valid
band = spectrum(3); // valid, in enumeration range[0, 4]
band = spectrum(999); // can be compiled, but invalid
band = spectrum(6.6); // invalid, not able to convert float to spectrum

指针

对每个指针变量,都需要用一个*来初始化。

int* p1, p2; // pointer p1 and integer p2;
int* p1, * p2; // two integer pointer p1 and p2

使用指针之前,必须对指针初始化。如:

int* addr;
cout << addr << *addr << endl; // runtime failure.

new为指针申请一块内存空间,称为堆(heap)或*存储区(free store),从而能够处理数据。

typename* pointer_name = new typename;

因而可以将之前的代码写成如下有效形式

int* addr = new int;
cout << addr << *addr << endl;
delete addr;

new的作用在于编译程序时能根据程序的调用情况,决定是否分配内存。称之为动态联编(dynamic binding)

使用new创建动态数组的方法如下:

typename* pointer_name = new typename[num_elements];    

关于数组名与指针的区别:

int arr[3] = {0};
arr = arr + 1;  // invalid
int* pt = arr;
pt = pt + 1;    // valid
cout << sizeof(arr) << sizeof(pt) << endl;

分别大小为12和4。那么如何定义指向数组地址的指针呢?

cout << &arr;    // display the address of whole array
int (*ptarr1)[3] = &arr;
auto ptarr2 = &arr;

此处ptarr是一个int指针数组,大小为3.
(ptarr1) == arr, (ptarr1)[0] == arr[0] == 100.

C风格字符串:

char str1[10] = "test";
char* str2 = str1;
cout << str2 << &str1 << (int*)str2 << &str2;

&str1 == (int)str2,即str2指向的是str1.通过(int)显式读取其地址。&str2为str2指针空间本身的地址。

一种bug的触发方式:未初始化申请后的堆空间

char* getstr(){
    char temp[80];
    cin.getline(temp, sizeof(temp));
    char* pt = new char[strlen(temp) + 1];
    // strcpy(pt, temp);    // 未初始化pt申请的堆空间,原始堆空间数据由0XCD填充,对应GB2312的“屯”字。因此会输出若干个“屯”
    return pt;
}    

变量存储机制

  • 自动存储
    来自函数局部变量,变量内存遵循后进先出的栈机制,即跳出函数后,其临时空间释放。
  • 静态存储
    一种是在函数外面定义之。
    另一种是在声明变量时使用static关键字。
  • 动态存储
    通过new关键字在heap中申请空间。如果不delete释放空间,则指针指向的空间会被一直占用,造成内存泄漏。如果泄漏空间的大小过大,则程序空间不足以正常运行,导致程序崩溃。

初始化结构数组

struct years{
    int year;
};

years temp[3] = {};

共用体

共用体union是一种变量共用存储空间的复合数据类型。

union id{
    int num;
    char name[5];
};

id temp;
strcpy(temp.name, "John");
cout << temp.name << endl;
temp.num = 0xCCCCCCCC;
cout << temp.name << endl;

先输出"John",然后输出"烫烫"。共用数据空间,因而原字符串数据被覆盖。

Chap 9. 内存模型与名称空间

自动变量

与代码块、函数块的进入与退出顺序一致,为后进先出顺序。

静态变量

以下实例:

int gldata = 50;       // static duration, external linkage 能够在外部文件使用该变量
static int stdata = 100;    // static duration, internal linkage 只能在本文件中使用该变量
int* test();

int main(){
    int* pt = test();
    (*pt)++;
    pt = test();
}

int* test(){
    static int temp;    // static duration, no linkage 函数退出后变量空间仍然存在,但不能显式调用该变量。可以存取该空间的数据。
    cout << temp << endl; // 静态变量未初始化前,所有位被设置为0,称为零初始化(zero-initialized)
    return &temp;
}

两次输出分别为0和1.

全局变量

在一个文件main函数外部定义变量,且不添加static关键字,即为全局变量。
在另一个文件使用extern时,能够显式地声明并链接该变量。注意,若为结构变量,这两个文件都必须调用相同的结构声明。

// COORDIN_H_
struct rect{
    double x;
    double y;
};

// coordin.cpp
rect test = {3.0, 4.0};

// main.cpp
#include "coordin.h"
extern rect test;    // 声明并链接coordin.cpp的变量。

void foo(){
    rect test;    // 在局部函数定义同名同类型变量,该局部变量会显式地隐藏该变量。
    printf("%2.2f %2.2f", test.x, test.y);        //访问局部变量。
    printf("%2.2f %2.2f", ::test.x, ::test.y);    //使用作用域解析运算符::可以指定访问全局变量。
}

注意,如果在一个文件的函数外部和函数内部都定义了static变量,则::无法访问到外部变量?

特别地,使用const定义的变量,其链接性为内部的。一般将const变量写在头文件里,cpp文件调用头文件,则每个文件都能得到相同定义,在该文件作用域内的const变量。

语言链接性

示例如下

extern "C" void foo();    // C语言链接性 C protocol for name look-up
extern void foo();        // C++         C++ protocol
extern "C++" void foo();  // C++         C++ protocol

new, delete的运算符重载

void* operator new(std::size_t);
void* operator new[](std::size_t);

在重载函数中,可根据输入参数调整传递给new的参数,具体调用的改变:

int* pi = new int;    // original
int* pi = new (sizeof(int));    // overload

显示字符串数组名的地址写法有:

char str[5];
cout << (int*) str << (void*) str;

void* 强制返回指针/变量的地址。

new的定位属性:让指针指向静态内存并使用静态内存空间存储变量。

#include <new>    // new的定位功能
const int N = 1000;
char buffer[N];
int *temp = new int[N];
delete []temp;
int *pi = new (buffer + N*sizeof(int)) int[N]; // 在内存地址buffer+N*sizeof(int)处开辟一块容纳N个int变量的内存空间
delete []pi;    // invalid 静态内存空间不能被释放。

命名空间

命名空间规定了作用域。

Chap 10. 对象和类

面向对象编程:Object Oriented Programming
OOP的特性:

  • 抽象
  • 封装与数据隐藏
  • 多态
  • 继承
  • 代码的可重用性

基本语法:

class Person{
private:
    std::string name_;
    int age_;
public:
    void SetPerson(const Person& p)
}; // 与结构排版相同,都需要分号结束

对成员函数进行定义时,使用作用域解析运算符::来标识函数所属的类。

void Person::SetPerson(const Person& p){
    ...
}

client/server 模型
server方/乙方只能修改类成员函数的实现而不能修改接口。
client方/甲方只能使用公有类函数。
这里与ros的client/server模型是相似的。ros的机制是client按照指定的输入-输出协议(可以理解为类)发送数据,server接收并处理,返回指定类型数据。

类的构造函数

构造函数的语法如下:

Person::Person(typename params){
    ...
}

一般需要设置默认参数,写法如下:

// 指定默认参数
Person::Person(const std::string name = "No name", int age = "0"){
    name_ = name;
    age_ = age;
}

或者:

// 声明
Person();    // 重构构造函数
Person(const std::string name, int age);
// 定义,类外实现
Person::Person(){
    name_ = "Default";
    age_ = 0;
}
Person::Person(const std::string name, int age){
    name_ = name;
    age_ = age;
}

要确保类所有成员有合理的初始值,所以推荐定义默认构造函数,而不是显式的提供默认参数。

类的析构函数(destructor)

析构函数在退出程序块时被隐式地调用,而当类通过new申请内存后,使用delete也会调用析构函数,因此不需要显式地调用。

// 声明
~Person();
// 定义,类外实现
Person::~Person(){
    std::cout << "Person " << name_ << "is destructed!\n";
}

析构函数的调用顺序遵循静态内存和动态内存的存储顺序。

  • 自动内存为栈,后进先出。
  • 静态内存为栈,后进先出,在整个程序结束时才被析构。(static)
  • 动态内存为堆,先进先出,程序结束时不会调用析构函数。调用delete时才能被析构。(new,delete)

注意:临时变量赋值也会被析构。

Person temp;
temp = Person("Stone", 1);

这里会构造一个无名变量Person("Stone", 1)并赋值到temp,然后临时变量被析构:

Person Stone is destructed!

这种赋值方式与以下并不一样。

Person temp = Person("Stone", 1);    //  视编译器而定,有可能是调用构造函数赋值,有可能是先创建临时变量。Visual Studio没有创建临时变量。

如果类变量被定义为const,即使void函数内没有修改调用对象,仍然无法通过编译。

temp.show();    // invalid

需要经过如下声明与定义:

void show() const;    // valid
// 类外定义
void Person::show() const{
    ...
}

指向调用对象自身的指针this

当涉及到自身与输入为同样的类进行数据交互时:

const Person& Person::oldest(const Person& person) const{
    if(age_ < person->age){
        return person;
    }
    else{
        return *this;
    }
}

这里还涉及到类作用域外需要加作用域符号::的问题。

Chap 11. 使用类

类内重载运算符

语法如下:返回类型 类::操作符(输入参数)

Time Time::operator+ (const Time& t)const{
...
}

友元

需要友元的原因:非成员函数需要类成员的访问权限。
使用方法:

  1. 将友元声明放在类声明中
  2. 友元函数不属于类函数,调用时不要使用类区域操作符::
  3. 友元函数不是类成员函数,不能使用修饰符const

operator<< 重载的可复用性
需要进行复用性更好的重载,应该返回值为ostream。

什么时候应该定义为类成员函数,什么时候应该使用友元函数?

类型转换函数:需要对operator typename()进行重载。

  • 转换函数必须是类方法
  • 转换函数不能指定返回类型
  • 转换函数不能有参数
    经验表明,这种类型转换函数是危险的,最好是写一个类成员的显式转换函数,在用户需要的时候调用。

从其他类型转换为本类型:对构造函数进行重载

Time::Time(typename input);

Chap 12. 类和动态内存分配

类静态成员的空间分配:单独开辟一块空间,且所有类的静态变量共享同一块空间

class Time{
private:
    static int second; // declaration
public:
    void PlusOneSec();
}

// definition
int Time::second = 0;

Time me;
me.PlusOneSec();
Time you;
you.PlusOneSec();

相同类型的类me和you都调用了方法PlusOneSec(),结果是second增加了2,因为共享内存空间。

复制构造函数与复制复制函数operator=

当函数按值传递调用含静态成员的类时,会引发严重的问题:当退出函数时,进行了析构!
并且,由于按值传递类变量,调用了默认构造函数Class(const Class& ),即含有指针的部分也是按值复制的,所以一旦退出函数,临时变量与外部变量指针相同
临时变量被析构(delete),因而造成外部类变量的指针成员没有指向的位置,变成野指针。

因而,要引入显式的针对指针变量的构造函数。

同样的,operator=也应该被重构。

如果需要读写类成员数组内的数据,也应该重构operator=.特别的,读取数据时,应该增加const限定符,从而兼容const变量。

class str{
private:
    char* ch_;
    int num_;
    const int max_num_;
public:
    str(const char* s);
};

str::str(const char* s){
    num_ = std::strlen(s);
    ch_ = new char[num_ + 1]; // 包括'\0'
    std::strcpy(ch_, s);
}

类内静态函数

例子如下:

// 类内声明

static int KnowSec();
// 类外定义;注意,不需要加static限定符
int Time::KnowSec(){
return second;
}

也体现了静态变量空间与自动变量空间的不同。

返回指向const对象的引用

例子:

const str& Max(const str& s1, const str& s2); // declaration
const str& str::Max(const str& s1, const str& s2){
    return (s1.num_ > s2.num_)?s1:s2;
}

类成员初始化列表与类内初始化

语法如下:

str::str(): ch_(nullptr), num_(0), max_num_(10)
{
}

只有构造函数能用这种语法。并且,对于const类成员,无法在构造函数中赋值,因此必须使用初始化列表的语法。

另一种方法是在类定义中进行初始化,但初始化列表的值会将其覆盖。

class str{
private:
    char* ch_;
    int num_ = 0;
    const int max_num_ = 100;
};

Chap 13. 类继承

derived 派生类

语法如下

class Derived : public Base{
private:
    typename newparam_;
public:
    Derived(typename params);
};
Derived : Derived(typename params): newparam_(newparam), Base(oldparams){
}

派生类构造函数:

  • 首先创建基类对象
  • 派生类构造函数应通过成员初始化列表,将基类参数传递至基类构造函数
  • 派生类构造函数应初始化派生类新增的数据成员

基类与派生类的关系

基类指针或引用可以指向派生类对象,但是不能调用派生类的函数和成员
派生类指针或引用不能指向基类对象。

继承

  • 公有继承:is-a(-kind-of)关系
    一般在程序设计上,为属于关系。
    公有继承不建立is-implemented-as-a关系。如可以使用数组来实现栈,但不能从Array派生出Stack。

多态性

  • 继承类可访问基类的公有函数与变量,无法访问基类的私有函数与变量。
  • 继承类可访问自身的所有函数与变量。
  • 继承类在自身虚函数内希望访问基类的同一个虚函数,必须添加作用域解析运算符。

virtual关键字的作用:

  • 若没有使用virtual关键字修饰基类函数,将根据引用或指针的类型调用对应是基类或派生类的函数。
  • 若使用virtual关键字修饰基类函数,将根据引用或指针指向的类型调用派生类的函数。
    示例:

    class Client{...};
    class VIPClient : public Client{...};
    Client* c1 = new VIPClient(params);
    c1->show(); // 若无virtual修饰基类Client的show函数,将调用基类的show函数。反之,则调用派生类VIPClient的show函数

这就是多态性的体现。

虚析构函数:基类指针指向的是派生类对象的话,首先调用派生类的析构函数,然后自动调用基类的析构函数
使用虚析构函数可以确保正确的析构函数序列被调用。

静态联编与动态联编

static binding v.s. dynamic binding
允许向上强制转换upcasting,如:

Client* c1 = new VIPClient();

但不允许向下强制转换downcasting,派生类存在的数据成员无法处理。
除非使用强制类型转换,如:

Client NimaWang;
VIPClient* c2 = (VIPClient*) &NimaWang;

并且此时需要有派生类构造函数显式使用基类作为输入变量。

对非虚方法采用静态联编

  • 非虚方法效率更高
  • 指出了不需要重新定义该函数。
    因此,虚函数只在需要重新定义派生类的函数时使用。

虚函数表

存储为类对象进行声明的虚函数的地址。
如:

  • 基类指针包含一个指针,该指针指向基类中所有虚函数的地址表。
  • 派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,虚函数表会保存新函数的地址。
    因而使用虚函数,在内存和执行速度会增加一定的成本。
  • 对每个含有虚函数的对象创建虚函数表的存储空间。
  • 调用虚函数需要额外对该函数寻址并跳转。

需要注意的是:

  • 构造函数不能是虚函数。创建派生类对象,派生类对象调用派生类的构造函数,然后在其中使用基类的构造函数。
  • 析构函数应当是虚函数,除非类不用做基类。因为派生类新增的数据成员在析构时也应当被处理。
  • 友元函数不能是虚函数,因为友元不是类成员,而只有成员函数才能是虚函数。如果设计中出现了问题,应该在友元函数内部使用类的虚函数。

经验上说:

  • 如果重新定义继承的方法,应当保证派生类函数原型与基类相同。
  • 但如果返回值为基类引用或指针,则可以修改为指向派生类的引用或指针,称之为返回类型协变(covariance of return type)
    如下所示:

    class Client{
    public:
    virtual Client& build(string name);
    };
    class VIPClient{
    public:
    virtual VIPClient& build(string name);
    };

  • 如果基类声明被重载了,应该在派生类中重新定义所有的基类版本。

访问控制: protected

如果基类的数据对象被声明为protected:

  • 派生类对象能够直接访问基类的protected成员变量
  • 类外无法直接访问protected成员变量

抽象基类(ABC, Abstract Base Class)

  • 当类声明中包含纯虚函数时,就不能创建该类的对象。
  • 能够创建抽象基类的指针,指向派生类对象。

包含指针的基类与派生类设计

当基类包含指针时:

  • 派生类不包含指针:可以直接使用默认的派生类析构函数,派生类析构函数会自动调用基类的析构函数。
  • 派生类包含指针:必须设计显式构造函数,将指针指向对象的内容复制到本体;必须设计析构函数,完成指针空间的释放。

派生类的动态内存分配(DMA, Dynamic Memory Allocating)

基类引用可指向派生类型。

成员函数的讨论

构造函数

  • 默认构造函数
  • 复制构造函数
  • 构造函数参数不同引起的转换
    按值传递对象与传递引用
    赋值运算符

Chap 14. C++中的代码重用

建立has-a关系的C++技术为组合(包含),即创建包含一个其他类对象的类。

explicit关键字

以Student类为例:

Student{
private:
    string name;
    valarray<double> scores;
public:
    Student(int n): name("NULL"), scores(n) {}
};

如果想为其中的分数赋值,而程序员手误:

Student foo("Shit", 10);
foo = 5; //应该写为foo[0] = 5; 示例未包括operator[]的重载

那么第二行会隐式调用构造函数,使foo的name被重置为"NULL",且scores的个数置为5.
因此,需要在构造函数前面加上explicit关键字,防止单参数构造函数的隐式转换。

  • 类成员函数的const关键字含义

在类的成员函数后加const关键字,为常量成员函数,表明该成员函数不会改变类成员的值。
如:

double Average() const;
  • 无参数构造函数的使用

在main函数中构造没有参数的类,类后不应该加括号。

多重继承:Multiple Inheritance(MI)

包含(组合) vs 多重继承

Class Student: private string, private valarray<double>{
private:
public:
};
  1. 包含方法显式命名数据成员;私有继承提供了无名称的子对象成员,使用类名而非成员名来标识构造函数
  2. 包含方法使用对象名来调用方法;私有继承使用类名和作用域运算解析符来调用方法
  3. 访问基类对象:私有继承通过强制类型转换访问子对象(subobject)

如Student类的Name()方法

const string &Student() const{
    return (const string &)*this;
}
  1. 访问基类的友元函数:需要强制类型转换。

一般而言:应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

  • 当需要重新定义虚函数时,可以使用私有继承。重新定义的函数将只能在类中使用,而不是公有的。

类内使用固定常量的语法: enum

示例:

class Instrument{
protected:
    enum {Types = 7};
public:
    ...
};

虚基类(virtual base class)用于多重继承

C++在基类是虚的时候,禁止信息通过中间类自动传递给基类!编译器必须在构造派生对象之前构造基类对象组件。

如果类有间接虚基类,除非只需要使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。

多重继承可能导致函数调用的二义性!

示例:

class SingingWaiter: public Waiter, public Singer{
public:
    SingingWaiter(const Worker &wk, int p = 0, int voice = other): Worker(wk), Waiter(wk, p), Singer(wk, voice) {}
}

混合使用虚基类与非虚基类

例子:
类B被用作类C和类D的虚基类,同时被用作类X与类Y的非虚基类。
类M是从类C,D,X,Y派生而来的。
在这种情况下,类M从虚派生祖先(C,D)共继承一个B类子对象。从非虚派生祖先(X,Y)分别继承各一个B类子对象,总共继承3个B类子对象。

  • 当类通过多条虚类途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的子对象和分别表示各条非虚途径的多个基类子对象。

上一篇:

下一篇: