zmqNut

明知会散落, 仍不惧盛开

0%

C++三大特性

35434

封装

简单的来说就是一个类包装了一些数据以及操作这些数据的代码的逻辑实体。在一个类对象内部,某些操作或者数据可以是私有的,也可以是公有的。私有的是不能被外界锁访问的。通过这种方式,对类的内部成员提供了不同级别的保护,尽可能的去隐藏掉内部细节,只提供给对外的一些接口,用户无需知道对象内部细节,只通过接口来访问。

  • 良好的封装可以减少耦合
  • 可以对成员进行更精确的控制

访问说明符

public: 该访问说明符之后的各个成员都可以被公开访问,简单来说就是无论类内还是类外都可以访问。

protected:该访问说明符之后的各个成员可以被类内、派生类或者友元的成员访问,但类外不能访问。

private: 该访问说明符之后的各个成员只能被类内成员或者友元的成员访问,不能被从类外或者派生类中访问。

在c++中,类(class)的所有成员的默认访问权限是私有(private),而结构(struct)的所有成员的默认访问权限是公共(public)

在实例化变量时设定初始值

为完成这种操作,需要定义默认构造函数(Default constructor)。若无显示的构造函数,则编译器认为该类有隐式的默认构造函数。即若无定义任何构造函数,则编译器会自动生成一个默认构造函数,并会根据成员元素的类型进行初始化(与定义内置类型变量相同)。

在这种情况下,成员元素都是未初始化的,访问未初始化的变量的结果是未定义的(也就是说并不知道会返回何值)。

一般来说,默认构造函数不带参,当然也可以被重载。如果已经定义了构造函数,且构造函数的参数列表不为空,那么编译器便不会再生成无参数的默认构造函数。,这可能会使试图以默认方法构造变量的行为编译失败。

使用C++11或以上时,可以使用{}进行变量初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class object{
int weight;
int value;
object() {
weight = 0;
value = 0;
}
object(int _weight = 0, int _value = 0) {
weight = _weight;
value = _value;
}
};
object A; //ok
object B(1, 2); //ok
object C{1, 2}; //ok,(C++11)

必须使用列表初始化的情况

使用初始化列表能够提高程序运行效率

  • const常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
  • 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
  • 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化
1
2
3
4
5
6
7
8
9
10
11
class Test1{
public:
Test1(int a):i(a){}
int i;
};
class Test2{
public:
Test1 test1 ;
Test2(Test1 &t1)
{test1 = t1;}
};

以上代码无法通过编译,因为Test2的构造函数中test1 = t1这一行实际上分成两步执行:
调用 Test1 的默认构造函数来初始化 test1
由于Test1没有默认的构造函数,所以test1 = t1 无法执行,故而编译错误。

正确的代码如下,使用初始化列表代替赋值操作:

1
2
3
4
5
class Test2{
public:
Test1 test1 ;
Test2(int x): test1(x){}
};

销毁

这是不可避免的问题。每一个变量都将在作用范围结束走向销毁。但对于已经指向了动态申请的内存的指针来说,该指针在销毁时不会自动释放所指向的内存,需要手动释放动态内存。如果结构体的成员元素包含指针,同样会遇到这种问题。需要用到析构函数来手动释放动态内存。

析构函数(Destructor)将会在该变量被销毁时被调用。重载的方法形同构造函数,但需要在前加~

友元

友元(friend):使用friend关键字修饰某个函数或者类。可以使得在被修饰者不成为成员函数或者成员类的情况下,访问该类的私有(private)或者受保护(protected)成员。简单来说就是只要带有这个类的friend标记,就可以访问私有或受保护的成员元素。

继承

使用已经存在的类作为基础去建立一个新类,新类的定义可以增加新的功能,也可以用父类的功能,但是不能选择性的继承,可以解决代码的复用问题,可以提高效率。采用继承的方式,继承原有的类,并在此基础上新增一些自己特有的成员与方法,称被继承的类为基类(父类),继承下来的类叫派生类(子类)。

  • 子类可以拥有父类非私有的属性和方法
  • 子类可以对父类进行扩展
  • 子类可以重写父类的方法

通过图来理解继承

定义一个基类:Person类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person //Person
{
public:
Person();//默以构造
char* GetName();
void SetName(char *name);
virtual ~Person();//默以折构
protected:
private:
char *m_name;
int m age;
bool m_gender;
};

定义完一个Person类作为基类后,再定义一个学生类,一个教师类来继承Person类如下图:

2353

从图中可知。Student类,Teacher类继承了Person类,我只把派生类中的新成员与新方法写了出来,但实际上,子类继承了父类的所有成员与方法。值得注意的是,子类虽然继承了父类的私有成员,但却不能直接访问须通过父类继承下来的方法来获取

派生类构造函数的写法

派生类虽然继承了基类,但却需要自己的构造函数与析构函数,因为子类中有特有的成员,光靠父类的构造函数是无法进行初始化的,派生类的构造函数有下面几种写法

  • 使用基类的默认构造
1
2
3
4
Student (形参列表):
{
//派生类中的新成员初始化
}

这样,程序会先调用基类的默认构造函数,在调用子类的构造函数;

  • 调用基类重载的构造函数
1
2
3
4
Teacher (形参列表):基类名(实参列表)
{
//派生类中的新成员初始化
}

编译器会根据实参列表选择基类相应的构造函数进行调用,其实参来自于Teacher的形参列表;

三种继承方式

前面提到了公有继承,也提到了基类的私有成员子类是无法访问的,下面就详细介绍继承方式以及相关成员的访问权限:

1234235

继承方式:C++默认继承方式是私有继承

2345

也就是说,基类的私有成员子类无论使用那种那个继承方式都不可以访问,而采用保护继承时,基类的共有与保护属性的变量都会变为保护属性。

多态

多态原理

C++支持两种多态性:编译时多态性,运行时多态性。

a.编译时多态性:通过重载函数实现

b.运行时多态性:通过虚函数实现。

C++的多态性,一言以蔽之就是:

在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。

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
#include <iostream>
using namespace std;
class Base {
public:
virtual void fun()
{
cout << " Base::func()" << endl;
}
};
class Son1 : public Base {
public:
void fun()
{
cout << " Son1::func()" << endl;
}
};
class Son2 : public Base {
};
int main()
{
Base *base = new Son1;
base->fun();
base = new Son2;
base->fun();
delete base;
base = NULL;

return 0;
}
// Son1::func()
// Base::func()

Base为基类,其中的函数为虚函数。子类1继承并重写了基类的函数,子类2继承基类但没有重写基类的函数,从结果分析子类体现了多态性,那么为什么会出现多态性,其底层的原理是什么?这里需要引出虚表虚基表指针的概念。

虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表

虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针

image-20220530213244041

实现多态的过程:

  1. 编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址

  2. 编译器会在每个对象前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数

  3. 所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表

  4. 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面

    这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。

​ C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区

动态绑定

静态绑定:编译时绑定,通过对象调用

动态绑定:运行时绑定,通过地址实现

只有采用“指针->函数()”或“引用变量.函数()”的方式调用C++类中的虚函数才会执行动态绑定。对于C++中的非虚函数,因为其不具备动态绑定的特征,所以不管采用什么样的方式调用,都不会执行动态绑定。

即所谓动态绑定,就是基类的指针或引用有可能指向不同的派生类对象,对于非虚函数,执行时实际调用该函数的对象类型即为该指针或引用的静态类型(基类类型);而对于虚函数,执行时实际调用该函数的对象类型为该指针或引用所指对象的实际类型。比如下面代码:

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
#include<bits/stdc++.h>
using namespace std;
class Base {
public:
void func() {
cout << "func() in Base." << endl;
}
virtual void test() {
cout << "test() in Base." << endl;
}
};

class Derived : public Base {
void func() {
cout << "func() in Derived." << endl;
}
virtual void test() {
cout << "test() in Derived." << endl;
}
};

int main() {
Base* b;
b = new Derived();
b->func();
b->test();
}
//func() in Base.
//test() in Derived.

b是一个基类指针,它指向了一个派生类对象,基类Base里面有两个函数,其中test为虚函数,func为非虚函数。因此,对于test就表现为动态绑定,实际调用的是派生类对象中的test,而func为非虚函数,因此它表现为静态绑定,也就是说指针类型是什么,就会调用该类型相应的函数

虚函数是动态绑定的基础;动态绑定是实现运行时多态的基础

要触发 动态绑定,需满足两个条件:

(1) 只有虚函数才能进行动态绑定,非虚函数不进行动态绑定。

(2) 必须通过基类类型的引用或指针进行函数调用

通过基类指针或基类引用做形参,当实参传入不同的派生类(或基类)的指针引用,在函数内部触发 动态绑定,从而来 运行时 实现多态的。

---------- End~~ 撒花ฅ>ω<*ฅ花撒 ----------