本笔记基于中国大学慕课《C/C++语言程序设计(下)(沈萦华老师)》及「OpenJudge - 信通学院编程小组」。

一、命名空间

——避免命名冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace A {
int x, y;
}
// 三种使用方式
void fun1() {
x = 8; // Error
A::y = 6; // OK
}
void fun2() {
using namespace A;
x = 8; // OK
y = 6; // OK
}
void fun3() {
using namespace A::y;
x = 8; // Error
y = 6; // OK
}

二、引用

引用

即别名:

1
2
3
4
5
6
const int maxN = 3e5 + 8;
int n = 1, m = 2;
int& a = n; // OK
int& b; b = n; // Error 需立即初始化
int& c = n; c = &m; // Error 不可重新命名
int& d = maxN; // Error 不可引用常量

例 1.1 调用传递引用的参数,实现两个字符串变量的交换。

1
2
3
4
5
6
7
8
void swap(char*& a, char*& b) {  
char* tmp = a; a = b; b = tmp;
}
int main() {
char* ap = (char*)"hello";
char* bp = (char*)"how are you?";
swap(ap, bp);
}

评注 在 C 语言中,需使用

1
2
3
4
5
6
7
8
void swap(char** a, char** b) {    
char* tmp = *a; *a = *b; *b = tmp;
}
int main() {
char* ap = (char*)"hello";
char* bp = (char*)"how are you?";
swap(&ap, &bp);
}

常引用

常引用不可修改:

1
2
3
4
5
6
7
int n = 8;
const int& a = n;
a = 6; // Error 不能修改常引用以修改引用的变量
n = 6; // OK
int& b = a; // Error 不能以 const T& 初始化 T&
int& c = (int&)a; // OK 强制转换后可以初始化
const int& b = a; // OK

三、函数

内联函数 inline

功能 任何调用函数的地方都换成了其代码。

作用 避免因为频繁大量的使用栈空间,而造成因栈空间不足所造成的程式出错的问题,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。

代码风格 它是用于实现的关键字:

1
2
3
4
5
6
7
8
9
inline void fun1(int n);
void fun1(int n) {

} // Ugly

void fun2(int n);
inline void fun2(int n) {

} // OK

适用情形 只适合函数体内代码简单的函数数使用:

  • 不能包含复杂的结构控制语句例如 whileswitch
  • 内联函数本身不能是直接递归函数。

不适用情形 内联是以代码膨胀为代价,因此以下情况不宜使用内联:

  • 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
  • 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

函数重载

名字相同但参数个数或参数类型不同的函数:

1
2
3
4
5
6
7
8
9
int max(double f1, double f2) { } // (1)
int max(int n1, int n2) { } // (2)
int max(int n1, int n2, int n3) { } // (3)
int main() {
max(3.4, 2.5); // 调用 (1)
max(2, 4); // 调用 (2)
max(1, 2, 3); //调用 (3)
max(3, 2.4); // Error
}

带默认参数的函数(函数的缺省参数)

作用 提高程序的可扩充性。

1
2
3
4
5
6
7
8
9
int fun(int x = 1, int y = 2);
int main() {
cout << fun(6, 8); // 14
cout << fun(6); // 8
cout << fun(); // 3
}
int fun(int x, int y) {
return x + y;
}

注意 在指定了默认值的参数的右边,不能出现没有指定默认值的参数。只能最右边连续的几个参数缺省。

传值、传址与传引用

例 2 修改程序。将其修改为传引用参数,并修改 findmax() 为非递归函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void findmax(int* a, int n, int i, int* pk);
int main() {
int a[10], n = 0;
for (int i = 0; i < 10; i++)
cin >> a[i];
findmax(a, 10, 0, &n);
}
void findmax(int* a, int n, int i, int* pk) {
if (i < n) {
if (a[i] > a[*pk])
*pk = i;
findmax(a, n, i + 1, &(*pk));
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
void findmax(int* a, int n, int i, int& pk);
int main() {
int a[10], n = 0;
for (int i = 0; i < 10; i++)
cin >> a[i];
findmax(a, 10, 0, n);
}
void findmax(int* a, int n, int i, int& pk) {
for (int i = 0; i < n; ++i) {
if (a[i] > a[pk])
pk = i;
}
}

例 3 分析代码,思考哪些可以实现两数交换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
} // 值作参数 交换函数参数值 swap1(a, b); Ugly
void swap2(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
} // 指针作参数 交换指针指向内容 swap2(&a, &b); OK
void swap3(int* a, int* b) {
int* temp = a;
a = b;
b = temp;
} // 指针作参数 交换指针 swap3(&a, &b); Ugly
void swap4(int& a, int& b) {
int temp = a;
a = b;
b = temp;
} // 引用作参数 交换参数值 swap4(a, b); OK

函数返回引用

函数作为左值:

1
2
3
4
5
6
7
8
9
int mx;
int& max(int a, int b) {
mx = a > b ? a : b;
return mx;
}
int main() {
int a = 6, b = 8;
cout << max(a, b);
}

例 4 生成有 10 个整数的安全数组。要把值放入数组中,使用 put() 函数,然后使用 get() 取出该值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int vals[10];
int error = -1;
int& put(int n) {
if (n >= 0 && n < 10) return vals[n];
cout << "RE";
return error;
}
int get(int n) {
if (n >= 0 && n < 10) return vals[n];
return error;
}
int main() {
int n; cin >> n;
if (put(n) == -1) return 0;
cin >> put(n);
cout << get(n) << endl;
return 0;
}

例 5

1
2
3
4
5
6
7
8
9
int& fun(int a) {
static int t = 0;
return t += 2 * a;
}
int main() {
int x = 1, y = 2;
cout << ++fun(x); // 3
cout << ++fun(y); // 8
}

四、类的基本概念

下列关键字说明类成员可被访问的范围:

1
2
3
private:  // 私有 成员函数内访问 (默认)
public: // 公有 任何地方访问
protected:

在类的成员函数内部,能够访问当前对象和同类其它对象的全部属性、函数;在类的成员函数以外的地方,只能够访问该类对象的公有成员。

使用类的成员变量和成员函数:

1
2
3
对象名.成员名
指针->成员名
引用名.成员名
  • 基类的 private 成员:
    • 基类的成员函数
    • 类的友元函数
  • 类的 public 成员:
    • 基类的成员函数
    • 基类的友元函数
    • 派生类的成员函数
    • 派生类的友元函数
    • 其他的函数
  • 基类的 protected 成员:
    • 基类的成员函数
    • 基类的友元函数
    • 派生类的成员函数可以访问当前对象和其它对象的基类的保护成员

五、构造函数与析构函数

  • 构造函数 初始化成员变量。构造函数需 public;构造函数名与类名相同;可定义不同的构造函数以初始化不同情形。
  • 拷贝构造函数 X::X(X& other) 当用类对象初始化另一对象、以类为参数调用函数或函数返回类时调用。在赋值时不起作用。
  • 类型转换构造函数 实现类型的自动转换。只有一个参数,而且不是复制构造函数的构造函数,一般看作转换构造函数。
  • 析构函数 在对象消亡(包括程序结束、函数参数消亡、在函数中作为临时对象返回)时调用,可用于 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
class mymyClass {  
private:
int value;
string name;

public:
// 构造函数
mymyClass(int val, const string& n) : value(val), name(n) {}

// 拷贝构造函数
mymyClass(const mymyClass& other) : value(other.value), name(other.name) {}

// 类型转换构造函数
mymyClass(int val) : value(val), name("Default") {}

// 析构函数
~mymyClass() {}
};

int main() {
mymyClass obj1(8, "Obj1"); // 使用构造函数
mymyClass obj2 = obj1; // 使用拷贝构造函数
mymyClass obj3(8); // 使用类型转换构造函数
}

六、静态成员 static

——为所有对象共享。

  • 静态成员本质上是 全局变量
  • 静态成员函数 不具体作用于某个对象,因此静态成员不需要通过对象就能访问。
  • 在静态成员函数中,不能访问非静态成员变量,也不能调用非静态成员函数。
  • sizeof 不会计算静态成员变量。

七、this 指针

当你进入 3427 后,你可以看见桌游、火锅等,但是你看不到 3427 的全貌了。
但 this 指针时时刻刻指向着 3427 本身。

——指向被调用的成员函数所属的对象。

1
2
3
4
5
6
7
8
9
10
11
class myClass {
public:
int num;
myClass(int num) {
this->num = num; // 当形参和成员变量同名时,可用 this 指针来区分
}
myClass& add(myClass other) {
this->num += other.num;
return *this; // 在类的非静态成员函数中返回对象本身
}
};
  • this 指针的本质是一个 指针常量
  • 每个非静态成员函数中都隐含着 this 指针,因此类的非静态成员函数,真实的参数比所写的参数多 1。
  • this 指针 只能在成员函数中使用,不能在全局函数、静态成员函数中使用(因为静态成员函数并不具体作用与某个对象)。
  • 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 myClassA {  
private:
string name;
public:
myClassA(const string& s) : name(s) {}
void print() const { cout << name << endl; }
};

class myClassB {
private:
int val;
public:
myClassB(int value) : val(value) {}
void print() const { cout << val << endl; }
};

class myEnclosedClass {
private:
myClassA classA;
myClassB classB;
public:
myEnclosedClass(const string& name, int val) : classA(name), classB(val) {}
void printAll() const {
classA.print();
classB.print();
}
};

int main() {
myEnclosedClass obj("Default", 8);
obj.printAll();
}

九、友元 friend

友元函数 一个类的友元函数可以访问该类的私有成员:

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
class myClassA;  // 前置声明 myClassA  

class myClassB {
public:
// myClassB 的成员函数,同时也是 myClassA 的友元
void displaynum(const myClassA& obj);
};

class myClassA {
private:
int num;
public:
myClassA(int value) : num(value) {}
// 声明 myClassB 的 displaynum 函数为友元
friend void myClassB::displaynum(const myClassA& obj);
};

// 定义 myClassB 中的友元函数
void myClassB::displaynum(const myClassA& obj) {
cout << "myClassA 的私有成员变量 num 的值为: " << obj.num << endl;
}

int main() {
myClassA a(8);
myClassB b;
b.displaynum(a); // 使用 myClassB 的对象调用友元函数访问 myClassA 的私有成员
return 0;
}

友元类 如果 A 是 B 的友元类,那么 A 的成员函数可以访问B的私有成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class myClass {  
private:
int num;
public:
myClass(int value) : num(value) {}
friend class myFriendmyClass;
};

class myFriendmyClass {
public:
void displaynum(myClass& obj) {
cout << obj.num << endl;
}
};

int main() {
myClass obj(8);
myFriendmyClass friendobj;
friendobj.displaynum(obj);
}

十、常量成员函数 const

——不能改变属性的值,也不能调用非常量成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class myClass {
private:
int num;
mutable val;
public:
void fun1() {}
void fun2() const {
num = 0; // Error 不能改变属性的值
val = 0; // OK mutable 成员变量可以在 const 成员函数中修改。
fun(); // Error 不能调用非常量成员函数
}
void fun2() {} // 允许重载
};

int main() {
const myClass obj;
obj.fun1(); // Error 常量对象只能使用构造函数、析构函数和有 const 说明的函数 (常量方法)
obj.fun2(); // OK
}

十一、重载运算符

重载运算符

1
2
3
4
5
6
7
8
9
10
11
class Complex {
double Re, Im;
Complex(double r = 0.0, double i = 0.0) :Re(r), Im(i) {}
Complex operator-(const Complex& b);
};
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.Re + b.Re, a.Im + b.Im); // 返回一个临时对象
} // 重载为普通函数
Complex Complex::operator-(const Complex& b) {
return Complex(Re - b.Re, Im - b.Im); // 返回一个临时对象
} // 重载为成员函数时 参数个数减一
  • 不允许定义新的运算符;
  • 对运算符进行重载的时候,应尽量保留运算符原本的特性;
  • ".", ".*", "::", "?:", "sizeof" 不能被重载;
  • 重载运算符 "()", "[]", "->", "="时,必须声明为类的成员函数。

重载赋值运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class myString {
private:
char* str;
public:
myString() :str(new char[1]) { str[0] = 0; }
const char* c_str() { return str; };
myString& operator = (const char* s);
~myString() { delete[] str; }
};
myString& myString::operator = (const char* s) { // 重载 "="
delete[] str;
str = new char[strlen(s) + 1];
strcpy(str, s);
return *this;
}
int main() {
myString s;
s = "Hello";
cout << s.c_str() << endl;
return 0;
}

浅拷贝和深拷贝

以上代码(浅拷贝,只是简单地复制了原对象的指针值)中,执行 s1=s2 后,s1.strs2.str 指向同一地方,因此需要补充拷贝构造函数的重载(深拷贝,分配新的内存来存储原对象的数据并复制原对象的数据到新分配的内存中):

1
2
3
4
5
6
myString& operator = (const myString& s) {
delete[] str;
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
return *this;
}

但为避免 s=s,需进一步补充:

1
2
3
4
5
6
7
myString& operator = (const myString& s) {
if (this == &s) return *this;
delete[] str;
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
return *this;
}

编写拷贝构造函数的时候,会面临和 = 同样的问题,用同样的方法处理。

1
2
3
4
myString(myString& s) {
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
}

总的代码:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#include <cstdlib>
#include <iostream>
using namespace std;
int strlen(const char* s) {
int i = 0;
for (; s[i]; ++i);
return i;
}
void strcpy(char* d, const char* s) {
int i = 0;
for (i = 0; s[i]; ++i)
d[i] = s[i];
d[i] = 0;

}
int strcmp(const char* s1, const char* s2) {
for (int i = 0; s1[i] && s2[i]; ++i) {
if (s1[i] < s2[i])
return -1;
else if (s1[i] > s2[i])
return 1;
}
return 0;
}
void strcat(char* d, const char* s) {
int len = strlen(d);
strcpy(d + len, s);
}
class MyString {
private:
char* str;
public:
MyString(const char* s = NULL) {
if (s) {
str = new char[strlen(s) + 1];
strcpy(str, s);
} else {
str = new char[1];
str[0] = '\0';
}
}
~MyString() {
if (str) delete[] str;
}
MyString(const MyString& s) {
if (s.str) {
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
} else {
str = new char[1];
str[0] = '\0';
}
}
MyString& operator= (const char* s) {
if (str == s) {
return *this;
}
delete[] str;
if (s) {
str = new char[strlen(s) + 1];
strcpy(str, s);
} else {
str = new char[1];
str[0] = '\0';
}
return *this;
}
MyString& operator=(const MyString& s) {
if (str == s.str) {
return *this;
}
delete[] str;
if (s.str) {
str = new char[strlen(s.str) + 1];
strcpy(str, s.str);
} else {
str = new char[1];
str[0] = '\0';
}
return *this;
}
friend MyString operator+ (const MyString& a, const MyString& b) {
char* tmp = new char[strlen(a.str) + strlen(b.str) + 1];
strcpy(tmp, a.str);
strcat(tmp, b.str);
return MyString(tmp);
}
friend ostream& operator<< (ostream& o, const MyString& s) {
o << s.str;
return o;
}
char& operator[] (int i) {
return str[i];
}
MyString& operator+= (const char* s) {
char* tmp = new char[strlen(str) + 1];
strcpy(tmp, str);
delete[] str;
str = new char[strlen(tmp) + strlen(s) + 1];
strcpy(str, tmp);
strcat(str, s);
return *this;
}
friend MyString operator+ (const char* p, const MyString& s) {
char* tmp = new char[strlen(p) + strlen(s.str) + 1];
strcpy(tmp, p);
strcat(tmp, s.str);
return MyString(tmp);
}
friend MyString operator+ (const MyString& s, const char* p) {
char* tmp = new char[strlen(s.str) + strlen(p) + 1];
strcpy(tmp, s.str);
strcat(tmp, p);
return MyString(tmp);
}
friend bool operator< (const MyString& s1, const MyString& s2) {
if (strcmp(s1.str, s2.str) < 0) {
return true;
} else {
return false;
}
}
friend bool operator> (const MyString& s1, const MyString& s2) {
if (strcmp(s1.str, s2.str) > 0) return true;
else
return false;
}
friend bool operator== (const MyString& s1, const MyString& s2) {
if (strcmp(s1.str, s2.str) == 0) return true;
else
return false;
}

char* operator() (int start, int length) {
char* tmp = new char[length + 1];
for (int i = start; i < start + length; ++i) {
tmp[i - start] = str[i];
}
tmp[length] = '\0';
return tmp;
}
};


int CompareString(const void* e1, const void* e2) {
MyString* s1 = (MyString*)e1;
MyString* s2 = (MyString*)e2;
if (*s1 < *s2)
return -1;
else if (*s1 == *s2)
return 0;
else if (*s1 > *s2)
return 1;
}
int main() {
MyString s1("abcd-"), s2, s3("efgh-"), s4(s1);
MyString SArray[4] = { "big","me","about","take" };
cout << "1. " << s1 << s2 << s3 << s4 << endl;
s4 = s3;
s3 = s1 + s3;
cout << "2. " << s1 << endl;
cout << "3. " << s2 << endl;
cout << "4. " << s3 << endl;
cout << "5. " << s4 << endl;
cout << "6. " << s1[2] << endl;
s2 = s1;
s1 = "ijkl-";
s1[2] = 'A';
cout << "7. " << s2 << endl;
cout << "8. " << s1 << endl;
s1 += "mnop";
cout << "9. " << s1 << endl;
s4 = "qrst-" + s2;
cout << "10. " << s4 << endl;
s1 = s2 + s4 + " uvw " + "xyz";
cout << "11. " << s1 << endl;
qsort(SArray, 4, sizeof(MyString), CompareString);
for (int i = 0; i < 4; i++)
cout << SArray[i] << endl;
//s1的从下标0开始长度为4的子串
cout << s1(0, 4) << endl;
//s1的从下标5开始长度为10的子串
cout << s1(5, 10) << endl;
return 0;
}

重载为友元函数

一般情况下,将运算符重载为类的成员函数,是较好的选择。但有时,重载为成员函数不能满足使用要求,重载为普通函数,又不能访问类的私有成员,所以需要将运算符重载为友元。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Complex {
double Re, Im;
public:
Complex(double r, double i) :Re(r), Im(i) {};
Complex operator+(double r);
friend Complex operator+ (double r, const Complex& c);
};
Complex Complex::operator+(double r) { // 解释 c+5
return Complex(Re + r, Im);
}
Complex operator+ (double r, const Complex& c) { // 解释 5+c
return Complex(c.Re + r, c.Im);
}

重载流插入运算符和流提取运算符

使用 ostream& ostream::operator<<(int n) 重载流插入运算符 <<,流提取运算符 >> 同理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Complex {
double Re, Im;
public:
Complex(double r = 0, double i = 0) :Re(r), Im(i) {};
friend ostream& operator<<(ostream& os, const Complex& c);
friend istream& operator>>(istream& is, Complex& c);
};
ostream& operator<<(ostream& os, const Complex& c) {
os << c.Re << "+" << c.Im << "i"; //以"a+bi"的形式输出
return os;
}
istream& operator>>(istream& is, Complex& c) {
string s;
is >> s; // 将"a+bi"作为字符串读入, “a+bi” 中间不能有空格
int pos = s.find("+", 0);
string stmp = s.substr(0, pos); // 分离出代表实部的字符串
c.Re = atof(stmp.c_str()); // atof库函数能将const char*指针指向的内容转换成 float
stmp = s.substr(pos + 1, s.length() - pos - 2); // 分离出代表虚部的字符串
c.Im = atof(stmp.c_str());
return is;
}

对于 << 操作符,通常将其 重载为全局函数,而不是作为 ostream 类的成员函数。这是因为 << 操作符的左侧通常是一个 ostream 对象(如 cout),而右侧是我们想要插入到流中的对象。如果 << 作为 ostream 的成员函数,那么它将只能访问 ostream 对象的内部状态,而不能直接访问其右侧的对象。对于 >> 同理。

重载类型转换运算符

1
2
3
4
5
6
class Complex {
double Re, Im;
public:
Complex(double r = 0, double i = 0) :Re(r), Im(i) {};
operator double() { return Re; }
};

重载自增、自减运算符

前置运算符作为一元运算符重载,即 T& operator++();(重载为成员函数)或 T1 & operator++(T2);(重载为全局函数);而后置运算符作为二元运算符重载,多写一个没用的参数,即 T operator++(int);(重载为成员函数)或 T1 operator++(T2, 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
class Complex {
double Re, Im;
public:
Complex(double r = 0.0, double i = 0.0) : Re(r), Im(i) {}
Complex& operator++() {
++Re;
return *this;
} // 前置++运算符重载作为成员函数
Complex operator--(int) {
Complex temp(*this);
--Im;
return temp;
} // 后置--运算符重载作为成员函数
friend Complex operator++(Complex& c, int);
friend Complex& operator--(Complex& c);
};
Complex operator++(Complex& c, int) {
Complex temp(c);
++c.Re;
return temp;
} // 后置++运算符重载作为全局函数
Complex& operator--(Complex& c) {
--c.Im;
return c;
} // 前置--运算符重载作为全局函数

十二、继承和派生

派生类

派生类拥有基类的全部成员函数和成员变量。使用

1
2
3
class 派生类名 : public 基类名 {

}

如果多种事物,有一些共同的特点,又有一些各自不同的特点,适合概括所有事物的共同特点,写一个基类。然后为每种事物写一个类,都从基类派生而来。

继承关系

基类 A、B 是基类 A 的派生类,逻辑上要求一个 B 对象也是一个 A 对象。

复合关系

类 B 中有成员变量 kk 是类 A 的对象,则 A 和 B 是复合,逻辑上要求 A 对象是 B 对象的固有属性或组成部分。

1
2
3
4
5
6
7
class CPoint {
double x,y;
};
class CCircle {
double r;
CPoint center;
};

循环定义的写法:

1
2
3
4
5
6
7
class CMaster; 
class CDog {
CMaster* pm;
};
class CMaster {
CDog* dogs[10];
};

派生类覆盖基类成员

覆盖 派生类可以定义一个和基类成员同名的成员。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号 ::。但一般基类和派生类不定义同名成员变量。

派生类的构造函数

在创建派生类的对象时,需要调用基类的构造函数初始化派生类对象中从基类继承的成员。

可在派生类的构造函数中,为基类的构造函数提供参数,或省略基类构造函数以自动调用基类的默认构造函数。

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
class Bug {
private:
int nLegs;
int nColor;
public:
int nType;
Bug(int legs, int color);
void PrintBug() {};
};
class FlyBug : public Bug { // FlyBug是Bug的派生类
int nWings;
public:
FlyBug(int legs, int color, int wings);
};
Bug::Bug(int legs, int color) {
nLegs = legs;
nColor = color;
}
// 错误的 FlyBug 构造函数
FlyBug::FlyBug(int legs, int color, int wings) {
nLegs = legs; // 不能访问
nColor = color; // 不能访问
nType = 1; // ok
nWings = wings;
}
// 正确的 FlyBug 构造函数
FlyBug::FlyBug(int legs, int color, int wings) :Bug(legs, color) {
nWings = wings;
}
int main() {
FlyBug fb(2, 3, 4);
fb.PrintBug();
fb.nType = 1;
fb.nLegs = 2; // error: nLegs is private
return 0;
}

在创建派生类的对象时,先执行基类的构造函数,用以初始化派生类对象中从基类 继承的成员;再执行成员对象类的构造函数,用以初始化派生类对象中 成员对象;最后执行派生类自己的构造函数。在派生类对象消亡时,先执行派生类自己的析构函数;再依次执行各成员对象类的析构函数;最后执行基类的析构函数。

继承的赋值兼容规则

public 继承:

1
2
3
4
class base {};
class derived : public base {};
base b;
derived d;
  • 派生类的对象可以赋值给基类对象 b = d;
  • 派生类对象可以初始化基类引用 base& br = d;
  • 派生类对象的地址可以赋值给基类指针 base* pb = & d;

protected 继承:基类的 public 成员和 protected 成员成为派生类的 protected 成员。不是 “是” 的关系。

private 继承:基类的 public 成员成为派生类的 private 成员,基类的 protected 成员成为派生类的不可访问成员。不是 “是” 的关系。

基类与派生类的指针强制转换

公有派生的情况下, 派生类对象的指针可以直接赋值给基类指针

1
Base* ptrBase = &objDerived;

ptrBase 指向的是一个 Derived 类的对象;
*ptrBase可以看作一个 Base 类的对象,访问它的 public 成员直接通过ptrBase 即可,但不能通过 ptrBase 访问 objDerived 对象中属于 Derived 类而不属于 Base 类的成员;

即便基类指针指向的是一个派生类的对象,也不能通过基类指针访问基类没有,而派生类中有的成员。

通过强制指针类型转换,可以把 ptrBase 转换成 Derived 类的指针

1
2
Base* ptrBase = &objDerived;
Derived* ptrDerived = (Derived*)ptrBase;

程序员要保证 ptrBase 指向的是一个 Derived 类的对象,否则很容易会出错。

直接基类与间接基类

在声明派生类时,只需要列出它的直接基类。派生类沿着类的层次自动向上继承它的间接基类。

派生类的成员包括:

  • 派生类自己定义的成员;
  • 直接基类中的所有成员;
  • 所有间接基类的全部成员。