F_JustWei's Studio.

C++ 面向对象高级编程

字数统计: 7.2k阅读时长: 30 min
2021/05/04 Share

C++ 面向对象高级编程

1、头文件结构

1.1 防卫式声明(防止头文件被重复包含)

在这里插入图片描述

1.2 前置声明(声明头文件中用到的类和函数)

在这里插入图片描述

1.3 类声明: 声明类的函数和变量,部分简单的函数可以在这一部分加以实现

在这里插入图片描述

1.4类定义: 实现前面声明的函数

在这里插入图片描述

2、访问权限

访问权限限定符只用于修饰类的成员变量和成员函数

访问权限分为三种,分别为publicprotectedprivate

访问权限 作用
public 可以被所有函数访问
protected 能被本类的函数子类的函数访问
private 只能被本类的函数访问
2.1 public

public限定符所修饰的成员变量和函数可以被类的函数、子类的函数、友元的函数以及类对象来访问,即可以使用成员运算符来访问。这里的友元函数,可以是该类的友元函数,也可以是该类的友元类的成员函数。

2.2 protected

protected限定符修饰的成员变量和成员函数可以被该类的成员函数访问,但是不能被类对象所访问,即不能通过类对象的成员运算符来访问。另外,这些成员可以被子类的函数和友元函数访问,相比public成员,少了一个可以使用类对象直接访问的特性。

2.3 private

private限定符修饰的成员变量只能被该类的方法和友元函数访问,子类函数无法访问,在这三个限定符中封装程度是最高的,一般来说,应该尽可能将类的成员变量声明为private而不是其他,减少成员变量的暴露,只提供gettersettter方法给外界访问,这样能提高类的安全性。

2.4 示例

类的声明内可以交叉定义多个不同级别的访问控制块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
// public访问控制块1
protected:
// protected访问控制块1
private:
// private访问控制块1
public:
// public访问控制块2
protected:
// protected访问控制块2
private:
// private访问控制块2
};

3、内联函数

C++中支持内联函数,其目的是为了提高函数的执行效率

设置为内联函数的两种方法:

  1. 在类声明内定义的函数,自动成为inline函数。
  2. 用关键字 inline放在函数定义的前面即可将函数指定为内联函数,内联函数通常就是将它在程序中的每个调用点上内联地展开,假设我们将MAX定义为内联函数:
1
2
3
4
5
6
7
8
inline int MAX(int a, int b) {
return a > b ? a : b;
}
//则调用:
cout << MAX(a, b) << endl;
//在编译时展开为:
cout << (a > b ? a : b) << endl;
//从而消除了把MAX写成函数的额外执行开销,提高函数的执行效率。
3.1 类声明内定义的函数,自动成为inline函数

在这里插入图片描述

3.2 类声明外定义的函数,需要加上inline关键字才能成为inline函数
1
2
3
inline int MAX(int a, int b) {
return a > b ? a : b;
}
3.3 内联函数的优点

当函数体比较小的时候,内联该函数可以令目标代码更加高效。对于存取函数以及其它函数体比较短,性能关键的函数,鼓励使用内联。

3.4 内联函数的缺点

滥用内联将导致程序变慢。内联可能使目标代码量或增或减,这取决于内联函数的大小。内联非常短小的存取函数通常会减少代码大小,但内联一个相当大的函数将戏剧性的增加代码大小。现代处理器由于更好的利用了指令缓存,小巧的代码往往执行更快。

3.5 内联函数的小结

inline只是编程者给编译器的一个建议,在编译时未必会真正被编译为inline函数。因此如果函数足够简单,我们就把它声明为inline就好了。

4、构造函数

与其他语言类似,C++的构造函数也可以有默认实参。C++构造函数的特殊之处在于列表初始化(initializationlist)。

使用成员初始化列表方法的时候需要注意,在初始化的时候需要按照声明的顺序进行初始化

4.1 列表初始化
1
2
3
4
5
6
7
class complex {
private:
double re, im;
public:
complex(double r = 0, double i = 0) :re(r), im(i)
{ }
};
4.2 赋值
1
2
3
4
5
6
7
8
9
10
class complex {
private:
double re, im;
public:
complex(double r = 0, double i = 0)
{
re = r;
im = i;
}
};
4.3 初始化与赋值的区别

C++ 规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,所以列表初始化的效率更高

5、值传递、指针传递与引用传递

5.1 值传递

形参是实参的深拷贝对形参的操作不会影响实参的操作。

5.2 指针传递

形参为指向实参地址的指针对形参的操作其实就是对实参的操作。

5.3 引用传递

形参相当于是实参的别名对形参的操作其实就是对实参的操作。

5.4 指针传递与引用传递的区别

指针能够毫无约束地操作内存中的任何东西,尽管指针功能强大,但是非常危险。

如果的确只需要借用一下某个对象的别名,那么就用引用,而不要用指针,以免发生意外。

5.5 值传递、指针传递与引用传递的小结

为了提高效率,使用引用传递参数,避免了参数的复制。

若不希望在函数体内对输入参数进行修改,应使用const修饰参数。

为提高效率,若函数的返回值是原本就存在的对象,则应以引用形式返回。若函数的返回值是临时变量,则只能通过值传递返回。

6、友元

友元函数不受访问级别的控制,可以自由访问对象的所有成员。
同一类的各个对象互为友元,因此在类定义内可以访问其他对象的私有变量。

使用友元时注意:

  • 友元关系不能被继承。
  • 友元关系是单向的,不具有交换性。若类 B 是类 A 的友元,类 A 不一定是类 B 的友元,要看在类中是否有相应的声明。
  • 友元关系不具有传递性。若类 B 是类 A 的友元,类 C 是 B 的友元,类 C 不一定是类A的友元,同样要看类中是否有相应的申明。
1
2
3
4
5
6
class complex
{
private:
friend class A;
friend complex& __doaml(complex*, const complex&);
};

7、操作符重载

operator 是 C++ 的一个关键字,它和运算符(如=)一起使用,表示一个运算符重载函数,在理解时可将 operator 和运算符(如operator=)视为一个函数名。

使用 operator 重载运算符,是 C++ 扩展运算符功能的方法。使用 operator 扩展运算符功能的原因如下:

  • 使重载后的运算符的使用方法与重载前一致。
  • 扩展运算符的功能只能通过函数的方式实现。(实际上,C++中各种功能都是由函数实现的)
7.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
class Complex {
private:
double re, im;
public:
Complex():re(),im()
{ }
Complex(double r = 0, double i = 0) :re(r), im(i)
{ }
bool operator==(const Complex& second) const
{
if (re == second.re && im == second.im) {
return true;
}
else {
return false;
}
}
};
int main(){
Complex a(2, 1);
Complex b(1, 1);
Complex c(2, 1);

cout << "a == b:" << (a == b) << endl;
cout << "a == c:" << (a == c) << endl;

return 0;
}
输出:
1
2
a == b:0
a == c:1
7.2 在类中声明需要重载的操作符,在类外定义
示例:
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
class Complex {
private:
double re, im;
public:
Complex():re(),im()
{ }
Complex(double r = 0, double i = 0) :re(r), im(i)
{ }
bool operator==(const Complex& second) const;
};
bool Complex::operator==(const Complex& second) const
{
if (re == second.re && im == second.im) {
return true;
}
else {
return false;
}
}
int main(){
Complex a(2, 1);
Complex b(1, 1);
Complex c(2, 1);

cout << "a == b:" << (a == b) << endl;
cout << "a == c:" << (a == c) << endl;

return 0;
}
输出:
1
2
a == b:0
a == c:1
7.3 在类外声明并定义需要重载的操作符
示例:
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 Complex {
private:
double re, im;
public:
Complex():re(),im()
{ }
Complex(double r = 0, double i = 0) :re(r), im(i)
{ }
double getRe()const
{
return re;
}
double getIm()const
{
return im;
}
};
bool operator==(const Complex& a, const Complex& b) {
if (a.getRe() == b.getRe() && a.getIm() == b.getIm()) {
return true;
}
else {
return false;
}
}
int main(){
Complex a(2, 1);
Complex b(1, 1);
Complex c(2, 1);

cout << "a == b:" << (a == b) << endl;
cout << "a == c:" << (a == c) << endl;

return 0;
}
输出:
1
2
a == b:0
a == c:1

8、析构函数

析构函数是构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动调用析构函数。
析构函数可用于释放对象时构造或在对象的生命期中所获取的资源。
不管类是否定义了自己的析构函数,编译器都自动执行类中非 static 数据成员的析构函数。

8.1 析构函数的特点
  • 名字与类名相同
  • 在前面需要加上 ~
  • 析构函数不能重载
  • 析构函数可以是inline函数
  • 析构函数没有返回值和参数列表
  • 析构函数应该设置为类的公有成员
  • 析构函数由系统自动调用,不能显式调用
  • 每个类有应该有一个析构函数,如果没有显式定义,那么系统会自动生成一个默认的析构函数
8.2 析构函数的用法
示例一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test
{
private:
int id;
public:
Test(int i) :id(i)
{ }
~Test()
{
cout << "ID: " << id << " destruction function is invoked!" << endl;
};
};
int main()
{
Test a(0);//栈中分配
Test b = Test(1);//栈中分配
Test* c = new Test(2);//堆中分配
delete c;
cout << "------End of Main-------" << endl;

return 0;
}
输出:
1
2
3
4
ID: 2 destruction function is invoked!
------End of Main-------
ID: 1 destruction function is invoked!
ID: 0 destruction function is invoked!
分析:

C++ 创建对象的三种不同方式:

1
2
3
Test a(0);//栈中分配
Test b = Test(1); //栈中分配内存,跟方法1相同,是方法1的完整模式
Test* c = new Test(2);//堆中分配

方式 1、2 中都是在栈中分配内存,在栈中内存由系统自动的去分配和释放,而使用new创建的指针对象是在堆中分配内存,当不需要该对象时,需要我们手动的去释放,否则会造成内存泄漏

在上述程序中, a 和 b 都是栈中的对象,在程序结束时由系统来释放,因此出现在 —-End of Main 之后。

c 是new出来的堆中对象,所以需要手动的 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
class Test
{
private:
int id;
public:
Test(int i) :id(i)
{ }
~Test()
{
cout << "ID: " << id << " destruction function is invoked!" << endl;
};
};

Test t0(0);//最先创建的对象,最后释放

void Func()
{
static Test t1(1);//创建静态对象,会在整个程序结束时自动释放
Test t2(2);//在Func结束时自动释放
cout << "-----Func-----" << endl;
}

int main()
{
Test t3(3);
t3 = 10;//类型转换构造函数,这里会创建临时对象,将int型转成Test类型对象,在赋值结束后,临时变量销毁
cout << "------Begin of Main-------" << endl;
{
Test t4(4);//花括号代表作用域,不需要等到main方法结束就释放了
}
Func();//进入Func函数
cout << "------End of Main-------" << endl;
return 0;
}
输出:
1
2
3
4
5
6
7
8
9
ID: 10 destruction function is invoked!
------Begin of Main-------
ID: 4 destruction function is invoked!
-----Func-----
ID: 2 destruction function is invoked!
------End of Main-------
ID: 10 destruction function is invoked!
ID: 1 destruction function is invoked!
ID: 0 destruction function is invoked!
8.3 new 和 delete 过程中的内存分配

new操作先分配内存,再调用构造函数

delete操作先调用析构函数,再释放内存

new[]delete[]应该配对使用。

9、static

对于类来说, non-static 成员变量每个对象均存在一份, static 成员变量和 static 成员函数在内存中仅存在一份。其中non-static成员函数通过指定this指针获得函数的调用权,而 static 函数不需要this指针即可调用。

static 成员函数可以通过对象调用,也可以通过类名调用。

static 成员变量需要在类声明体外进行初始化.

1
2
3
4
5
6
7
8
9
10
11
12
class Account {
public:
static double m_rate;
static void set_rate(const double& x) { m_rate = x; }
};
double Account::m_rate = 8.0;

int main() {
Account::set_rate(5.0);
Account a;
a.set_rate(7.0);
}

10、类之间的关系

10.1 继承(Generalization)

继承是面向对象的三大特征之一,是一种最能体现面向对象代码复用的类关系,对于继承,可以使用”is-a”来表示,比如,小轿车(类B)”is-a”车(类A),是对车(类A)的进一步刻画,那么这两个类就是继承关系。

1
2
3
class Goose : public Bird{
//子类扩展属性和方法
};
10.2 组合(Composition)

组合是将一个对象(部分)放到另一个对象里(组合)。它是一种 “has-a” 的关系。相比聚合,组合是一种强所属关系,组合关系的两个对象往往具有相同的生命周期,被组合的对象是在组合对象创建的同时或者创建之后创建,在组合对象销毁之前销毁。一般来说被组合对象不能脱离组合对象独立存在,而且也只能属于一个组合对象。比如,鸟类和翅膀类就是组合关系,在创建一个鸟类对象时,一定要同时或之后创建一个翅膀类对象,销毁一个鸟类对象时,一定要先同时或之前销毁翅膀对象。

在C++语法中,使用在一个类中包含另外一个类类型的成员来实现组合:

1
2
3
4
5
class Wing{
};
class Bird{
Wing wing;
};
10.3 聚合(Aggregation)

聚合是一种弱所属关系,比如一只大雁和雁群,就是一种聚合关系。和组合相比,被聚合的对象可以属于多个聚合对象,比如,一只大雁可能属于多个雁群。
在C++语法中,通过类的指针来实现聚合:

1
2
3
4
5
6
class Goose{
};
class Geese{
public:
Goose member[10];
};
10.4 关联(Association)

关联也是一种弱关系,但并不是从属关系,关联的类可以看作是平等的,比如一只大雁和老鹰的关系,就可以看作关联关系
C++中,通过定义其他类指针类型的成员来实现关联,下面是双向关联的实现方法:

1
2
3
4
5
6
class Egle{
class Goose *food;
};
class Goose{
class Egle *predator;
};
10.5 依赖(Dependency)

一个对象的某种行为依赖于另一个类,比如,大雁的迁徙行为受季节影响,那么,大雁和季节就会有依赖关系。
C++语法中,通过将一个类作为另一类方法的参数的形式实现两个类之间的依赖关系:

1
2
3
4
5
6
class Season{
};
class Goose{
public:
void Migrate(Season season); //或Migrate(Season *season)、Migrate(Season &season)
};
10.6 实现(Realization)

实现对应的是面向对象中的接口,即动物都要移动,但是每种移动的方式不一样,鸟要实现自己独有的移动的方法。
在C++中,接口通过的纯虚函数来实现,C++的多态就是通过虚函数来实现的:

1
2
3
4
5
6
7
8
9
class Animal{
public:
vitual void move();
};
class Bird: public Animal{
void move(){
//鸟的移动方式,飞
}
};

11、虚函数

成员函数有3种:非虚函数、虚函数和纯虚函数

  • 非虚函數(non-virtual function):不希望子类重新定义(override)的函数。
  • 虚函數(virtual function):子类可以重新定义(override)的函数,且有默认定义。
  • 纯虚函數(pure virtual function):子类必须重新定义(override)的函数,没有默认定义。
1
2
3
4
5
6
class Shape {
public:
virtual void draw() const = 0;//纯虚函数
virtual void error(const std::string& msg);//虚函数
int objectID() const;//非虚函数
};

C++虚函数详解

12、转化函数

转换函数分为两类:

  • 将本类型转换为其他类型。
  • 将其他类型转换为本类型。
12.1 将本类型转换为其他类型

定义操作符类型名()(例如:double())即可指定将本类型变量转换为其他类型的函数。

示例:
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 Fraction {
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {

}
~Fraction() {

}
operator double() const { // 重载类型转换运算符 double()
return (double)(m_numerator * 1.0 / m_denominator);
}

private:
int m_numerator; // 分子
int m_denominator; // 分母
};
int main()
{
Fraction f(1, 5);
cout << "f:" << double(f) << endl;
double d = 0 + f;
cout << "d:" << d << endl;

return 0;
}
输出:
1
2
f:0.2
d:0.2
解析:

对于0 + f,编译器可能会去寻找以下重载了运算符+的两个函数

1
2
Fraction::operator+(double);
operator+(Fraction, double);

若这两个函数均没找到。编译器就去寻找能否将Fraction类型转换为double类型。找到了类型转换函数Fraction::operator double(),发生了隐式转换。

12.2 将其他类型转换为本类型。

通过隐式调用构造函数将其他类型的变量转换为本类型。

示例:
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 Fraction {
public:
Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {

}
~Fraction() {

}
Fraction operator+(const Fraction& f) const { // 重载运算符 +
int t = lcm(m_denominator, f.m_denominator);
return Fraction(m_numerator * (t / m_denominator) + f.m_numerator * (t / f.m_denominator), t);
}

private:
int m_numerator; // 分子
int m_denominator; // 分母
friend ostream& operator<<(ostream&, const Fraction&);
};
ostream& operator<<(ostream& os, const Fraction& f) {
return os << f.m_numerator << "/" << f.m_denominator;
}
int main()
{
Fraction f(1, 5);
Fraction d = f + 1;
cout << f << endl;
cout << d << endl;

return 0;
}
输出:
1
2
1/5
6/5
解析:

在上面例子中,编译器找不到函数Fraction::operator+(int),就退而求其次,先隐式调用Fraction类的构造函数将4转换为Fraction类型变量,再调用Fraction::operator+(Fraction)函数实现+运算。

12.3 使用 explicit 避免隐式转换
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 Fraction {
public:
explicit Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) {

}
~Fraction() {

}
Fraction operator+(const Fraction& f) const { // 重载运算符 +
int t = lcm(m_denominator, f.m_denominator);
return Fraction(m_numerator * (t / m_denominator) + f.m_numerator * (t / f.m_denominator), t);
}

private:
int m_numerator; // 分子
int m_denominator; // 分母
friend ostream& operator<<(ostream&, const Fraction&);
};
ostream& operator<<(ostream& os, const Fraction& f) {
return os << f.m_numerator << "/" << f.m_denominator;
}
int main()
{
Fraction f(1, 5);
Fraction d = f + 1;
cout << f << endl;
cout << d << endl;

return 0;
}

13、伪指针

伪指针(pointer-like classes)是指作用类似于指针的对象,实现方式是重载*->运算符。

标准库中的shared_ptr类是一个典型的伪指针类,代码如下:

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
template<class T>
class shared_ptr {
public:
T& operator*() const { // 重载 * 运算符
return *px;
}

T* operator->() const { // 重载 -> 运算符
return px;
}
//...

private:
T *px;
// ...
};
int main()
{
int* px = new Foo;
shared_ptr<int> sp(px);

func(*sp); // 语句1: 被解释为 func(*px)
sp->method(); // 语句2: 被解释为 px -> method()


return 0;
}

14、伪函数

伪函数(function-like classes)是指作用类似于函数的对象,实现方式是重载()运算符,标准库中的几个伪函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T>
struct identity {
const T& operator()(const T &x) const { return x; }
};

template<class Pair>
struct select1st {
const typename Pair::first_type& operator()(const Pair &x) const { return x.first; }
};

template<class Pair>
struct select2nd {
const typename Pair::second_type& operator()(const Pair &x) const { return x.second; }
};

15、模板

15.1 类模板

类模板实例化时需要指定具体类型。

示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
class complex {
public:
complex(T r = 0, T i = 0) : re(r), im(i) {

}

T real() const { return re; }
T imag() const { return im; }

private:
T re, im;
};
int main()
{
// 类模板实例化时需要指定具体类型
complex<double> c1(2.5, 1.5);
complex<int> c2(2, 6);

return 0;
}
15.2 函数模板

在调用时编译器会进行参数推导(argument deduction),因此不需要指定具体类型。

示例:
1
2
3
4
5
6
7
8
9
10
11
12
template<class T>
inline const T& min(const T& a, const T& b) {
return b < a ? b : a;
}
int main()
{
// 函数模板实例化时不需要指定具体类型
min(3, 2);
min(complex(2, 3), complex(1, 5));

return 0;
}
15.3 成员模板

用于指定成员函数的参数类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T1 second_type;

T1 first;
T2 second;

pair() : first(T1()), second(T2()) {}
pair(const T1& a, const T2& b) : first(a), second(b) {}

template<class U1, class U2>
pair(const pair<U1, U2>& p) : first(p.first), second(p.second) {}
};

这种结构通常用于实现子类到父类的转换.

在这里插入图片描述

16、模板特化

模板特化用来部分针对某些特定参数类型执行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<class Key>
struct hash {
// ...
};

template<>
struct hash<char> {
size_t operator()(char x) const { return x; }
};

template<>
struct hash<int> {
size_t operator()(char x) const { return x; }
};

template<>
struct hash<long> {
size_t operator()(char x) const { return x; }
};

上述代码实现针对charintlong这三个数据类型使用指定代码创建对象,其它数据类型使用默认的泛化操作创建对象。

17、模板偏特化

模板偏特化有两种形式:

  1. 个数的偏::指定部份参数类型。
  2. 范围的偏::缩小参数类型的范围。
17.1 个数的偏
1
2
3
4
5
6
7
8
9
10
template<typename T, typename Alloc>
class vector{
// ...
};

template<typename Alloc>
class vector<bool, Alloc>{ // 指定了第一个参数类型
// ...
};

17.2 范围的偏
1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class C{
// 声明1...
};

template<typename T>
class C<T*>{ // 指定了参数类型为指针类型
// 声明2...
};

C<string> obj1; // 执行声明1
C<string*> obj2; // 执行声明2

18、引用

声明引用(reference)时候必须赋初值,指定其代表某个变量,且之后不能再改变改引用的指向。对引用调用=运算符同时改变引用和其指向变量的值,不改变引用的指向。

示例:
1
2
3
4
5
6
int x = 0;
int *p = &x;
int &r = x; // r代表x,现在r,x都是0
int x2 = 5;
r = x2; // r不能重新代表其他变量,现在r,x都是5
int &r2 = r; // 现在r2,r,x都是5(r2和r都代表x)
其内存结构如下所示:

请添加图片描述

18.1 引用的假象:

虽然在实现上,几乎所有的编译器里引用的底层实现形式都是指针,但C++制造了以下两个假象,确保对于使用者来说引用和其指向的变相本身是一致的:

  • 引用对象和被指向的对象的大小相同(sizeof(r)==sizeof(x))
  • 引用对象和被指向的对象地址相同(&x==&r)
示例:
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
typedef struct Stag { int a, b, c, d; } S;

int main() {
double x = 0;
double *p = &x; // p指向x,p的值是x的地址
double &r = x; // r代表x,现在r,x都是0

cout << sizeof(x) << endl; // 8
cout << sizeof(p) << endl; // 4, 指针大小为4字节
cout << sizeof(r) << endl; // 8, 假象: r的大小和x相同,屏蔽了r底层的指针

cout << p << endl; // 0065FDFC, x的地址
cout << *p << endl; // 0
cout << x << endl; // 0
cout << r << endl; // 0
cout << &x << endl; // 0065FDFC
cout << &r << endl; // 0065FDFC, 假象: r的地址就是x的地址,屏蔽了r底层的指针

S s;
S &rs = s;
cout << sizeof(s) << endl; // 16
cout << sizeof(rs) << endl; // 16
cout << &s << endl; // 0065FDE8
cout << &rs << endl; // 0065FDE8

return 0;
}
18.2 引用的用途

引用被用作美化的指针。

在编写程序时,很少将变量类型声明为引用,引用一般用于声明参数类型(paramete rtype)和返回值类型(return type)。

注意:因为引用传递参数和值传递参数的用法相同,所以两个函数的函数签名(signature)相同,不能同时存在。

1
2
3
4
5
6
7
8
9
10
// 参数类型声明为引用,不影响函数体内使用变量的方式
void func1(Cls obj) { opj.xxx(); } // 值传递参数
void func2(Cls *Pobj) { pobj->XXX(); } // 指针传递参数,函数体内使用变量的方式需要修改
void func3(Cls &obj) { obj.xxx(); } // 引用传递参数,函数体内使用变量的方式与值传递相同

// 参数类型声明为引用,不影响参数传递的方式
Cls obj;
func1(obj); // 值传递参数
func2(&obj); // 指针传递参数,传递参数时需要对参数作出修改
func3(obj); // 引用传递参数,传递参数时不需对参数做出修改

有意思的是,指示常量成员函数的const也是函数签名的一部分,因此constnon-const的同名成员函数可以在同一类内共存。

19、对象模型

理解对象模型,才能真正理解多态动态绑定

示例:
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 A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1;
int m_data2;
};

class B : public A {
public:
virtual void vfunc1();
void vfunc2();
private:
int m_data3;
};

class C : public B {
public:
virtual void vfunc1();
void vfunc2();
private:
int m_data1;
int m_data4;
};

在内存中的布局如下图所示:

请添加图片描述

先看成员变量部分:对于成员变量来说,每个子类对象重都包含父类的成分,值得注意的是,C 类的 m_data1 字段和父类 A 类的字段 m_data1 相同,这两个字段共存于 C 类的对象中。

再看函数的部分,每个含有虚函数的对象都包含一个特殊的指针 vptr ,指向存储函数指针的虚表 vtbl 。编译器根据vtbl表中存储的函数指针找到虚函数的具体实现。这种编译函数的方式被称为动态绑定。

19.1 静态绑定和动态绑定

  • 对于一般的非虚成员函数来说,其在内存中的地址是固定的,编译时只需将函数调用编译成call命令即可,这被称为静态绑定。

请添加图片描述

  • 对于虚成员函数,调用时根据虚表vtbl判断具体调用的实现函数,相当于先把函数调用翻译成(*(p->vptr)[n])(p),这被称为动态绑定。

请添加图片描述

虚函数触发动态绑定的条件是同时满足以下3个条件:

  1. 必须是通过指针来调用函数。
  2. 指针类型是对象的本身父类。
  3. 调用的是虚函数。

20、常量成员函数

不改变成员变量的成员函数被称为常量成员函数,在函数体前需要有 const 修饰,若常量成员函数不加以 const 修饰,常量对象就无法调用该函数。

是否可以调用 常量对象(const object) 可变对象(non-const object)
常量成员函数(保证不修改成员变量)
非常量成员函数(有可能修改成员变量)

指示常量成员函数的 const 被视为函数签名的一部分,也就是说 const 和 non-const 版本的同名成员函数可以同时存在。

两个版本的同名函数同时存在时,常量对象只能调用 const 版本的成员函数,非常量对象只能调用 non-const 版本的成员函数。

在STL的string类重载[]运算符时,就同时写了constnon-const版本的实现函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class template std::basic_string<...> {
// ...

charT operator[] (size_type pos) const { // 常量成员函数,只有常量对象才能调用该函数
// 不用考虑copy on write
// ...
}

reference operator[] (size_type pos) { // 非常量成员函数,只有非常量对象才能调用该函数
// 需要考虑copy on write
// ...
}
}
CATALOG
  1. 1. C++ 面向对象高级编程
    1. 1.0.1. 1、头文件结构
      1. 1.0.1.0.1. 1.1 防卫式声明(防止头文件被重复包含)
      2. 1.0.1.0.2. 1.2 前置声明(声明头文件中用到的类和函数)
      3. 1.0.1.0.3. 1.3 类声明: 声明类的函数和变量,部分简单的函数可以在这一部分加以实现
      4. 1.0.1.0.4. 1.4类定义: 实现前面声明的函数
  2. 1.0.2. 2、访问权限
    1. 1.0.2.0.1. 2.1 public
    2. 1.0.2.0.2. 2.2 protected
    3. 1.0.2.0.3. 2.3 private
    4. 1.0.2.0.4. 2.4 示例
  • 1.0.3. 3、内联函数
    1. 1.0.3.0.1. 3.1 类声明内定义的函数,自动成为inline函数
    2. 1.0.3.0.2. 3.2 类声明外定义的函数,需要加上inline关键字才能成为inline函数
    3. 1.0.3.0.3. 3.3 内联函数的优点
    4. 1.0.3.0.4. 3.4 内联函数的缺点
    5. 1.0.3.0.5. 3.5 内联函数的小结
  • 1.0.4. 4、构造函数
    1. 1.0.4.0.1. 4.1 列表初始化
    2. 1.0.4.0.2. 4.2 赋值
    3. 1.0.4.0.3. 4.3 初始化与赋值的区别
  • 1.0.5. 5、值传递、指针传递与引用传递
    1. 1.0.5.0.1. 5.1 值传递
    2. 1.0.5.0.2. 5.2 指针传递
    3. 1.0.5.0.3. 5.3 引用传递
    4. 1.0.5.0.4. 5.4 指针传递与引用传递的区别
    5. 1.0.5.0.5. 5.5 值传递、指针传递与引用传递的小结
  • 1.0.6. 6、友元
  • 1.0.7. 7、操作符重载
    1. 1.0.7.0.1. 7.1 在类中声明并定义需要重载的操作符
    2. 1.0.7.0.2. 示例:
    3. 1.0.7.0.3. 输出:
    4. 1.0.7.0.4. 7.2 在类中声明需要重载的操作符,在类外定义
    5. 1.0.7.0.5. 示例:
    6. 1.0.7.0.6. 输出:
    7. 1.0.7.0.7. 7.3 在类外声明并定义需要重载的操作符
    8. 1.0.7.0.8. 示例:
    9. 1.0.7.0.9. 输出:
  • 1.0.8. 8、析构函数
    1. 1.0.8.0.1. 8.1 析构函数的特点
    2. 1.0.8.0.2. 8.2 析构函数的用法
    3. 1.0.8.0.3. 示例一:
    4. 1.0.8.0.4. 输出:
    5. 1.0.8.0.5. 分析:
    6. 1.0.8.0.6. 示例二:
    7. 1.0.8.0.7. 输出:
    8. 1.0.8.0.8. 8.3 new 和 delete 过程中的内存分配
  • 1.0.9. 9、static
  • 1.0.10. 10、类之间的关系
    1. 1.0.10.0.1. 10.1 继承(Generalization)
    2. 1.0.10.0.2. 10.2 组合(Composition)
    3. 1.0.10.0.3. 10.3 聚合(Aggregation)
    4. 1.0.10.0.4. 10.4 关联(Association)
    5. 1.0.10.0.5. 10.5 依赖(Dependency)
    6. 1.0.10.0.6. 10.6 实现(Realization)
  • 1.0.11. 11、虚函数
  • 1.0.12. 12、转化函数
    1. 1.0.12.0.1. 12.1 将本类型转换为其他类型
    2. 1.0.12.0.2. 示例:
    3. 1.0.12.0.3. 输出:
    4. 1.0.12.0.4. 解析:
    5. 1.0.12.0.5. 12.2 将其他类型转换为本类型。
    6. 1.0.12.0.6. 示例:
    7. 1.0.12.0.7. 输出:
    8. 1.0.12.0.8. 解析:
    9. 1.0.12.0.9. 12.3 使用 explicit 避免隐式转换
  • 1.0.13. 13、伪指针
  • 1.0.14. 14、伪函数
  • 1.0.15. 15、模板
    1. 1.0.15.0.1. 15.1 类模板
    2. 1.0.15.0.2. 示例:
    3. 1.0.15.0.3. 15.2 函数模板
    4. 1.0.15.0.4. 示例:
    5. 1.0.15.0.5. 15.3 成员模板
  • 1.0.16. 16、模板特化
  • 1.0.17. 17、模板偏特化
    1. 1.0.17.0.1. 17.1 个数的偏
    2. 1.0.17.0.2. 17.2 范围的偏
  • 1.0.18. 18、引用
    1. 1.0.18.0.1. 示例:
    2. 1.0.18.0.2. 其内存结构如下所示:
    3. 1.0.18.0.3. 18.1 引用的假象:
    4. 1.0.18.0.4. 示例:
    5. 1.0.18.0.5. 18.2 引用的用途
  • 1.0.19. 19、对象模型
    1. 1.0.19.0.1. 示例:
  • 1.0.20. 20、常量成员函数