1.引用

引用变量

引用变量是一个别名,也就是说,他是某个已经存在的另一个名字。一旦引用初始化为某个变量,就可以使用名称或变量名称来指向变量。

引用很容易和指针混淆,但是他们不同。

  1. 不存在空引用。引用必须连接到一个合法的内存。【指针可以是野指针】
  2. 一旦引用被初始化为一个对象,就不能指向另一个对象。指针可以在任何时候指向另一个对象
  3. 引用必须在创建初始化。指针可以在任何时间被初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;//此时改变b的值就是改变a的值
cout << a << endl;
cout << b << endl;
printf(" %p\n %p\n",&a,&b);
b = 20;
cout << a << endl;
cout << b << endl;
return 0;
}

image-20230509205947796

这里发现,a 和 b 的地址是一样的,也就是&b就是a的地址,所以a和b的值是一样的。

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
#include<iostream>
using namespace std;
void swap(int a, int b)//a,b不改变,因为a,b的⽣命周期只在swap函数内,结束swap后a,b就消失,不改变实参
{
int c = 0;
c = a;
a = b;
b = c;
}
void swap1(int* a, int* b)//使⽤指针,交换地址实现数字变换
{
int c = 0;
c = *a;
*a = *b;
*b = c;
}
void swap2(int& a, int& b)//使⽤引⽤,a,b是实参的别名,相当于对实参的调⽤
{
int c = 0;
c = a;
a = b;
b = c;
}
int main()
{
int x = 1;
int y = 2;
swap(x, y);
cout << x << "," << y << endl;
swap1(&x, &y);
cout << x << "," << y << endl;
swap2(x, y);
cout << x << "," << y << endl;
return 0;
}
image-20230509211016416

这里第一个swap()方法也就传入两个形参的函数,这里并没有实现x,y的交换,因为a,b的生命周期只在swap函数内,结束swep后,a,b就消失,不改变实参。

而sweap1()则是实现了x,y值的交换,因为这里用了指针,传入的是x和y的地址,然后在函数取值进行交换,使得x和y的值被交换

而sweap2()也是起到了x 和 y值的交换,因为它是以引用方式进了x和y的值,相当于对实参的调用,最后输出了1和2.这是因为上面的指针已经将x和y交换了2和1,之后又调用sweap2()把x和y又交换了回来。并不是没有交换

引用函数

当函数返回值为引用的时候,若返回栈变量,不能成为其他引用的初始值,不能作为左值使用

若返回静态变量或者全局变量,可以成为其他引用的初始值,即可作为右值使用,也可以作为左值使用

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
#include<iostream>
using namespace std;
int geta()
{
int a = 10;
return a;
}
int& geta1()
{
int a = 20;
return a;
}
void main()
{
int a1 = 0;
int a2 = 0;
a1 = geta();
cout << "a1:" << a1 << endl;
a2 = geta1();
cout << "a2:" << a2 << endl;
int& a3 = geta1();
cout << "a3:" << a3 << endl;

//geta1() = 100;
//cout << "a3:" << a3 << endl;
}

image-20230509211915311

这里第一个就不赘述了。

第二个,int& geta1()这个方法。&说明返回的是a的引用,换句话说就是返回a本身,所以这里a2输出的是a2:20.

同理a3只是换一个写法int &类型,返回值也是a3:20

这里如果补充一个,将函数引用作为左值,结果输出:

1
2
geta1() = 100;
cout << "a3:" << a3 << endl;

image-20230509212435781

总结:

1.在应用的使用中,单纯给某个变量去别名是毫无意义的。应用的目的主要在于用于函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题

2.用引用传递函数的参数,能保证参数在传递的过程中不参数副本,从而提高传递效率,同时通过const的使用,还可以保证参数在传递过程中的安全性

3.引用本身是目标变量或对象的别名,对引用的操作本质上是对目标变量或者对象操作。因此能使用引用时就别用指针。

2.虚表

首先,我们要知道这个虚表是用在什么地方的。

面向对象程序设计中有继承的概念。

继承

1
2
3
4
5
6
7
8
9
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派⽣类
class Dog : public Animal {
// bark() 函数
};
image-20230509215115821

1.基类private成员在派生类中无论以什么方式都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类队对象中,但是在语法上限制了派生类对象不管在类里面还是在外面都不能去访问它

2.基类orivate成员在派生类中是不能被访问的,如果基类成员不想在类外直接访问,但需要在派生类中能访问,就可以定义为protected。所以,保护成员protected是因为继承才出现的。

3.表格里面的访问方式都是最小的”权限”

4.在使用关键字class的时候默认的继承方式是:private,使用struct的默认方式是public,不过最好写出继承的方式

5.在实际运行中一般都是使用public继承。

基于Public的继承方式:

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
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:
Student(string s, int g, int a)
{
cout << "Constuct Student" << endl;
name = s;
grade = g;
age = a;
}
void print()
{
cout << "Student:" << endl;
cout << "name=" << name << endl;
cout << "grade=" << grade << endl;
cout << "age=" << age << endl;
}
protected:
string name;
int grade;
private:
int age;
};
class GraduateStudent :public Student //继承
{
public:
GraduateStudent(string s, int g, int a) :Student(s, g, a) //调⽤基类的构造函数,构造基类
{
cout << "Constuct GraduateStudent" << endl;
}
void print1()
{
cout << "GraduateStudent:" << endl;
cout << "name= " << name << endl;
cout << "grade= " << grade << endl;
//cout << "age=" << age << endl;
}
};
void main()
{
GraduateStudent g("Ouyang", 95, 21);
g.print(); //⼦类可以直接访问基类公共成员成员
g.print1();
system("pause");
}
image-20230509220257123

这里可以看到子类可以访问父类的方法也可以调用自身的方式。但是父类如果定义的是Private/Protected的函数或则属性则⼦类不可访问。

  • 基类的私有成员:子类不可以访问
  • 基类的白哦胡成员,子类可以继承为自己的保护成员,在派生类可以访问,在外部不可以访问,
  • 基类的共有成员,子类可以继承为自己的共有成员。在派生类可以访问,在外部也可以访问

3.虚函数

用virtual关键字修饰的函数就是虚函数

vTable(虚表)是C++利用runtime来实现多态的工具,所以我们需要借助virtual关键字将函数代码的地址存入vTable来躲开静态编译期。【不懂】

看一个例子:

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
#include <iostream>
#include <ctime>
using std::cout;
using std::endl;
struct Animal { void makeSound() { cout << "动物叫了" << endl; } };
struct Cow : public Animal {
void makeSound() {
cout << "⽜叫了" << endl;
}
};
struct Pig : public Animal {
void makeSound() {
cout << "猪叫了" << endl;
}
};
struct Donkey : public Animal {
void makeSound() {
cout << "驴叫了" <<
endl;
}
};
int main(int argc, const char* argv[])
{
srand((unsigned)time(0));
int count = 4;
while (count--) {
Animal* animal = nullptr;
switch (rand() % 3) {
case 0:
animal = new Cow;
break;
case 1:
animal = new Pig;
break;
case 2:
animal = new Donkey;
break;
}
animal->makeSound();
delete animal;
}
return 0;
}

image-20230509220914036

这里会连续执行4次Animal的makeSound()方法

因为我们基类Animal的makeSound()方法没有用virrual修饰,所以静态编译的时候makeSound()的实现就定死了。调用makeSound()方法的时候,编译器发现这是Animal指针,就会直接jump到makeSound()的代码地址去调用

我们修改一下

1
2
struct Animal {virtual void makeSound() { cout << "动物叫了" << endl; }
};

image-20230509221728962

⾸先我们需要知道⼏个关键点:

  1. 函数只要有virtual,我们就需要把它添加进vTable。

  2. 每个类(⽽不是类实例)都有⾃⼰的虚表,因此vTable就变成了vTables。

  3. 虚表存放的位置⼀般存放在模块的常量段中,从始⾄终都只有⼀份。

Cow Pig Donkey中他们都重写了makeSound()函数,所以当他们碰到调⽤makeSound()函数时候他们就会去jump到⾃⼰的code中去运⾏,当他们⾃身中没有重写的函数时,它们就会Jump到⽗类的⽅法中去运⾏。