专业课笔记——(第九章:类的封装、继承、多态)

目录

一、类与结构体

1.类与结构体

2.new与delete

3.类的封装访问属性

二、类的封装

1.构造函数

2.析构函数

3.静态成员

4.友元

5.运算符重载

三、类的继承

1.继承和派生

2.继承中的构造与析构

3.多继承中的二义性

四、类的多态

1.多态

2.函数的重载、重写、和重定义


一、类与结构体

1.类与结构体

1、类与结构体的主要区别:

  • 类是引用类型,结构是值类型。
  • 结构不支持继承。
  • 结构不能声明默认的构造函数。

2、结构和类的适用场合:

  • 当堆栈的空间很有限,且有大量的逻辑对象时,创建类要比创建结构好一些;
  • 对于点、矩形和颜色这样的轻量对象,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构的成本较低;
  • 在表现抽象和多级别的对象层次时,类是最好的选择,因为结构不支持继承。

2.new与delete

1、new的用法:

  • new int;(开辟一个存放整数的存储空间,返回一个指向该存储空间的地址)
  • new int(100);(开辟空间,并初始化整数为100,返回地址)
  • new char[10];(开辟字符数字空间,返回首元素地址)创建数组对象时,不能为对象指定初始值。
  • new int[5][5];(开辟二维数组空间,返回首元素地址)
  • float *p=new float(3.14159);(开辟空间,赋初值,并将地址赋给指针变量p)

2、delete的用法:释放已分配的空间

  • (delete 指针变量)(delete[] 指针变量)(指针变量必须是一个new返回的地址)

3.类的封装访问属性

1、结构体能做的事类都能做,类能做的事结构体不一定能做(本质是:结构体内不能封装函数,但类可以)(故在c++中学会了类也就学会了结构体)

2、封装对内开放数据,对外屏蔽数据,对外提供接口,从而达到信息的隐蔽作用。

3、封装访问属性:

  • 用struct定义类的时候,其所有成员默认为public。
  • 用class定义类的时候,其所有成员默认为private。

|---------------|--------|----------|----------|
| 访问属性 | 属性 | 对象内部 | 对象外部 |
| public | 公有 | 可访问 | 可访问 |
| protected | 保护 | 可访问 | 不可访问 |
| private | 私有 | 可访问 | 不可访问 |

二、类的封装

1.构造函数

1、构造函数:

  • 构造函数在对象创建时自动调用且只调用一次**,完成初始化相关工作。**
  • 无返回值,与类名相同,默认无参数,可以重载,可以默认有参数。
  • 一经实现,默认将不复存在。
  • 拷贝构造函数的参数一定是引用类型。

2、无参构造函数、带参构造函数、拷贝构造函数的实现。

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	int age;
	
	Person()//无参构造函数
	{
		cout << "Person 无参构造函数的调用" << endl;
	}

	Person(int a)//有参构造函数
	{
		age = a;
		cout << "Person 有参构造函数的调用" << endl;
	}
	
	Person(const Person &p)//拷贝构造函数
	{
		//将传入的人身上的所有属性,拷贝在我的身上 
		age = p.age;
		cout << "Person 拷贝构造函数的调用" << endl;
	}
	
	//析构函数不可以有参数,不可以发生重载
	//对象销毁前 会自动调用析构函数 而且只会调用一次
    ~Person()
	{
		cout << "Person 析构函数的调用" << endl;
	}
};

void test01()
{
	Person p1; //默认构造函数的调用
	Person p2(10); //有参构造函数的调用
	Person p3(p2); //拷贝构造函数的调用

	//注意事项1:调用默认构造函数时候,不用加()
	//因为下面这行代码,编译器会认为是一个函数的声明
	//Person p1();

	cout << "p2的年龄为:" << p2.age << endl;
	cout << "p3的年龄为:" << p3.age << endl;
}

int main()
{
	test01();
	system("pause");
	return 0;
}
cpp 复制代码
//运行结果:
Person 无参构造函数的调用
Person 有参构造函数的调用
Person 拷贝构造函数的调用
p2的年龄为:10
p3的年龄为:10
Person 析构函数的调用
Person 析构函数的调用
Person 析构函数的调用

2.析构函数

1、析构函数

  • 构造函数是在实例化的时候被调用,而析构函数在对象销毁的时候被调用。
  • 在对象销毁前会自动调用析构函数,而且只会调用一次,目的时清空数据,释放内存,防止内存外泄。
  • 无返回值,与类名相同,前面带一个 ~ 符号,无参数,不可重载默认参数。
  • 析构函数的作用并不是删除对象,而是在对象销毁前完成一些清理工作。

2、析构函数的实现

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	Person()
	{
		cout << "Person 构造函数的调用" << endl;
	}
	//1.2、析构函数没有返回值 不写void
	//析构函数名和类名相同 在名称前加~
	//析构函数不可以有参数,不可以发生重载
	//对象销毁前 会自动调用析构函数 而且只会调用一次
	~Person()
	{
		cout << "Person 析构函数的调用" << endl;
	}
};

void test01()
{
	Person P;
}
int main()
{
	test01();
	system("pause");
	return 0;
}

3.静态成员

1、静态成员分为静态成员变量和静态成员函数。静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。

2、静态成员属于整个类而不是某个对象,类的静态成员属于类也属于对象,最终归于类。

3、静态成员变量

  • 所有对象共享同一份数据,实现了同类对象的信息共享
  • 在编译阶段分配内存;
  • 类内声明,类外初始化(类外存储,求类大小,并不包括在内);
  • 可以通过类名访问,也可通过对象访问。

4、静态成员的实现

  • 静态成员函数只能访问静态成员变量,无法访问非静态成员变量。 (因为非静态成员函数在调用的时候this指针会被当作函数参数进行传递,而静态成员函数属于类不属于对象,没有this指针,故无法进行访问
  • private权限,类外无法访问。
  • 不在类内部初始化 。
  • 静态函数意义不是信息共享,而在于数据的沟通,管理静态数据成员,完成对静态数据成员的封装。
cpp 复制代码
#include<iostream>  
using namespace std;  
  
class person
{  
public:  
    static int m_A;  
    static void func()
    {  
        m_C = 100;  
// 		m_D = 200;报错,在类中m_B无法赋值,因为静态成员函数只能访问静态成员变量;
        cout << "func函数" << endl;  
    }  
    static int m_C; //不在类内部初始化  
    int m_D;
  
private:  
    static int m_B;//private权限,类外无法访问
    static void func2()//func2()是private,类外无法访问
    {  
        cout << "func2函数" << endl;  
    }  
};  
  
//在类外部初始化静态成员变量  
int person::m_A = 100;  
int person::m_B = 200;  
int person::m_C = 0;  
  
int main() 
{  
    person p;  
    cout << p.m_A << endl;  
    cout << person::m_A << endl;  
// 	cout << person::m_B << endl;此处报错,private权限类外无法访问
    p.func();  
    person::func();  
//  p.func2();// 注意:func2()是private,无法从 main 直接调用  
    return 0;  
}

4.友元

1、友元分为:友元函数和友元类。 (目的:提高程序的运行效率,但是破坏了类的封装性和隐蔽性,使得非成员函数可以访问类的私有函数)

2、友元函数、友元类定义(友元=友元函数+友元类):

  • 友元的声明位置没有要求,可以在private、protected、public权限区,效果都是一样的;
  • 友元是单向的,A在B类中被声明为友元,表示A是B的友元,但B不是A的友元;
  • 友元具有和类成员一样的权限,可以访问protected和private权限的成员,但不是类的成员;

3、友元函数

  • 友元函数在类中声明时用friend修饰,但是在定义时不需要用friend修饰;
  • 友元函数不能被继承:父类的友元函数,继承后并不会成为子类的友元函数;
  • 友元函数不具有传递性:A类和B类都是C类的友元类,但是A类和B类并不是友元类;

4、优缺点:

  • 缺点:友元函数不是类的成员但是却具有成员的权限,可以访问类中受保护的成员,这破坏了类的封装特性和权限管控;(代码更灵活,但是不能滥用)
  • 优点:可以实现类之间的数据共享;

5、友元函数的实现

cpp 复制代码
#include <iostream>
using namespace std;

class Person{
private:
	int age;

public:
	Person(){};
	Person(int x);
	friend void print(Person &pn);//声明print是友元函数
};

Person::Person(int x){
	this->age = x;
}

void print(Person &pn){
	//因为print是Person类的友元函数,所以在内部可以访问Person类的私有成员age
	cout << "age=" << pn.age << endl;
}

int main(void)
{
	Person p(22);
	print(p);
	return 0;
}

6、友元类的实现

cpp 复制代码
#include <iostream>  
using namespace std;  
  
class Person {  
private:  
    int age;  
  
public:  
    Person() {}  
    Person(int x) : age(x) {} // 使用初始化列表初始化age  
    friend class Printer;// 声明Printer类为Person的友元类  
};  

class Printer {  
public:  
    // 因为Printer是Person的友元类,所以可以访问Person的私有成员  
    void print(Person &pn) {  
        cout << "age=" << pn.age << endl;  
    }  
};  
  
int main(void) {  
    Person p(22);  
    Printer printer;  
    printer.print(p);  
    return 0;  
}

5.运算符重载

1、重载的理解:

  • 在一个作用域内,可以声明几个功能类似的同名函数(也就是"一名多用"),但这些同名函数的形式参数(参数的个数、类型或顺序)必须不同。

2、总结重载:

  • 函数名相同。
  • 参数个数不同,参数类型不同,参数顺序不同,均可以构成重载。
  • 如果只有返回值类型不同的时候则不可以构成重载。

3、运算符重载:运算符重载的实质就是函数重载。

  • 就是编译软件本身的运算符不符合我们想要的运算符定义,我们需要重新定义运算符来达到我们的目的。
  • 运算符重载格式类型:
cpp 复制代码
返回类型 operator 运算符(参数列表)
{
    函数体 ;
}

4、重载运算符规则:

  • C++中不允许用户定义新的运算符,只能对已有的运算符进行重载。

  • 重载后的运算符的优先级、结合性也应该保持不变,也不能改变其操作个数和语法结构。

  • 重载后的含义,与操作基本数据类型的运算含义应类似,如加法运算符"+",重载后也应完成数据的相加操作。

  • 有5个运算符不可重载:类关系运算符":"、成员指针运算符"*"、作用域运算 符"::"、sizeof运算符、三目运算符"?:"

  • 运算符重载函数不能有默认参数,否则就改变了运算符操作数的个数,是错误的。

  • 用于类对象的运算符一般必须重载,但有两个例外("="和"&"不必重载)。

  • 运算符重载函数既可以作为类的成员函数,也可以作为类的友元函数(全局函数)。

三、类的继承

1.继承和派生

1、继承:所谓继承就是在一个已存在的类的基础上建立一个新的类。

  • 在继承关系中被继承的类称为基类(或父类),把通过继承关系创建出来的新类称为派生类(子类)。
  • 派生类对基类对象的访问由继承方式和成员性质决定。
  • 利用类的继承,可以将原来的程序代码重复使用,从而减少了程序代码的冗余度,符合软件重用的目标,提高软件开发效率。
  • 派生类不仅可以继承原来类的成员,还可以**增加新的数据成员、增加新的成员函数、**重新定义已有成员函数、改变现有成员的属性。
  • 继承具有传递性,即派生类能自动继承上层基类的全部数据结构及操作方法(数据成员及成员函数)

2、继承与派生的对应关系:

  • 一个派生类只从一个基类派生,这是最常见的继承形式
  • 一个派生类有两个及两个以上的基类。如:类C从类A和类B派生。

3、继承方式有public(公有继承)、protected(保护继承)和private(私有继承)

  • **public(公有继承):**基类的公有成员和保护成员被继承为派生类成员时,其访问属性不变。
  • protected(保护继承):基类的公有成员和保护成员在派生类中成了保护成员私有成员仍为基类私有。
  • private(私有继承):基类中的公有成员和保护成员在派生类中皆变为私有成员
  • 无论哪种继承方式,基类的私有成员均不能继承。这与私有成员的定义是一致的,符合数据封装的思想。

2.继承中的构造与析构

1、构造顺序(析构是相反的顺序):

  • 先构造父类,再构造成员变量,最后构造自己。
  • 先构造自己,再构造成员变量,最后构造父类。
cpp 复制代码
#include <iostream>  
using namespace std;  
  
//基类 Object  
class Object {  
public:  
    Object(const char* s) {  
        cout << "Object(" << s << ")" << endl;  
    }  
    ~Object() {  
        cout << "~Object()" << endl;  
    }  
};  
  
//派生类 Parent,继承自 Object  
class Parent : public Object {  
public:  
    Parent(const char* s) : Object(s) {  
        cout << "Parent(" << s << ")" << endl;  
    }  
    ~Parent() {  
        cout << "~Parent()" << endl;  
    }  
};  
  
//派生类 Child,继承自 Parent  
class Child : public Parent {  
    Object o1; // 成员变量  
    Object o2; // 成员变量  
        
public:        
    Child(const char* s) : Parent(s), o1("o1"), o2("o2") { 
    //按照声明顺序初始化成员变量 o1 和 o2(每个都会调用 Object 的构造函数)
        cout << "Child(" << s << ")" << endl;  
    }  
    ~Child() {  
        cout << "~Child()" << endl;  
    }  
};  
  
int main() {  
    Child c("Parameter from Child!");  
    // 析构顺序与构造顺序相反  
    return 0;  
}
cpp 复制代码
//输出结果如下所示:
Object(Parameter from Child!)
Parent(Parameter from Child!)
Object(o1)
Object(o2)
Child(Parameter from Child!)
~Child()
~Object()
~Object()
~Parent()
~Object()

1、构造函数调用顺序:

  • 首先调用基类 Object 的构造函数(通过 Parent 的构造函数传递参数)。
  • 然后调用 Parent 的构造函数。
  • 接着,在 Child 的构造函数体内,按照声明顺序初始化成员变量 o1 和 o2(每个都会调用 Object 的构造函数)。
  • 最后,调用 Child 的构造函数体。

2、析构函数调用顺序:

  • 与构造函数顺序相反,首先调用 Child 的析构函数。
  • 然后是成员变量 o2 和 o1 的析构函数(按照声明顺序的逆序)。
  • 接着是 Parent 的析构函数。
  • 最后是 Object 的析构函数(在 Parent 的析构函数中隐式调用,因为 Parent 继承自 Object)。

3.多继承中的二义性

1、继承中的二义性问题所在:

  • 相当于初始化了2个变量,而且地址不同,互相不影响,因此不知道访问哪个地址的变量,进而出现二义性的问题。
  • 解决二义性的两种方法:一种是直接限定空间区域、一种是虚继承。
  • C++提供虚继承机制,就是防止继承关系中成员访问的二义性。
  • 多继承提供了软件重用的强大功能,也增加了程序的复杂性。
cpp 复制代码
#include <iostream>  
  
class Base1 {  
public:  
    void show() {  
        std::cout << "Base1::show()" << std::endl;  
    }  
};  
  
class Base2 {  
public:  
    void show() {  
        std::cout << "Base2::show()" << std::endl;  
    }  
};  
  
class Derived : public Base1, public Base2 {  
public:  
    void test() {//这里出错了  
        show(); //尝试调用show,这里将产生二义性错误  
    //相当于初始化了2个变量,而且地址不同,互相不影响,因此不知道访问哪个地址的变量  
    }  
};  
  
int main() {  
    Derived d;  
    d.test();  
    return 0;  
}

2、直接限定空间区域方法

cpp 复制代码
#include <iostream>  
  
class Base1 {  
public:  
    void show() {  
        std::cout << "Base1::show()" << std::endl;  
    }  
};  
  
class Base2 {  
public:  
    void show() {  
        std::cout << "Base2::show()" << std::endl;  
    }  
};  
  
class Derived : public Base1, public Base2 {  
public:  
    void test() {  
        Base1::show(); // 调用Base1的show  
        Base2::show(); // 调用Base2的show  
    }  
};  
  
int main() {  
    Derived d;  
    d.test();  
    return 0;  
}

3、虚继承方法

cpp 复制代码
#include <iostream>  
  
class Base {  
public:  
    void showBase() { // 假设我们还有一个不同的函数来展示虚继承的效果  
        std::cout << "Base::showBase()" << std::endl;  
    }  
};  
  
class Base1 : virtual public Base { // 虚继承自Base  
public:  
    void show() {  
        std::cout << "Base1::show()" << std::endl;  
        showBase(); // 调用Base的showBase  
    }  
};  
  
class Base2 : virtual public Base { // 虚继承自Base  
public:  
    void show() {  
        std::cout << "Base2::show()" << std::endl;  
        showBase(); // 调用Base的showBase  
    }  
};  
  
class Derived : public Base1, public Base2 {  
public:  
    // 使用作用域解析运算符解决二义性  
    void test() {  
        Base1::show(); // 调用Base1的show  
        Base2::show(); // 调用Base2的show  
    }  
};  
  
int main() {  
    Derived d;  
    d.test();  
    return 0;  
}

四、类的多态

1.多态

**1、C++中的多态:**由继承而产生的相关的不同的类,其对象对同一消息会做出不同的响应。

2、多态成立的条件(缺一不可):------>(结果:派生类函数覆盖父类函数)

  • 要有继承(父类到子类之间的继承)
  • 要有虚函数重写(有关键字virtual关键字)
  • 要有父类指针(父类引用)指向子类指针

3、多态实现

cpp 复制代码
#include <iostream>  
using namespace std;  
  
// 定义基类  
class Parent {  
public:  
    virtual void print() { // 声明为虚函数  
        cout << "Parent:print() do." << endl;  
    }  
    virtual ~Parent() {} // 虚析构函数,用于安全删除派生类对象  
};  
  
// 定义派生类  
class Child : public Parent {  
public:  
    void print() override { // 重写基类的虚函数  
        cout << "Child:print() do." << endl;  
    }  
};  
  
// 定义一个函数,接收Parent类型的指针  
void function(Parent* p) {  
    p->print(); // 通过基类指针调用虚函数,实现多态  
}  
  
int main() {  
    // 首先是普通调用  
    // 输出Parent:print() do. 多态未发生,因为这里的指针指向的是基类对象  
    Parent* pp = new Parent();  
    pp->print();  
  
    // 多态发生,满足条件:基类指针指向派生类对象  
    Parent* pc = new Child();  
    pc->print(); // 输出:Child:print() do.  
  
    // 在函数中作用体现更明显  
    Parent* pcf = new Child();  
    function(pcf); // 输出:Child:print() do.  
    
    //代码逻辑:
    //当基类指针 pc 或 pcf 指向一个 Child 类的对象时,调用 pc->print() 或 pcf->print()
    //会执行 Child 类的 print() 函数。这是因为虽然指针的类型是 Parent*,
    //但它实际上指向的是一个 Child 类的对象。由于 print() 是虚函数,
    //所以在运行时,C++ 运行时系统会根据指针实际指向的对象的类型
    //来确定调用哪个版本的 print() 函数。这就是多态性的体现。

    return 0;  
}

2.函数的重载、重写、和重定义

1、重载(Overloading)

定义 :在同一个作用域内,允许存在多个同名函数,只要这些函数的参数列表不同(参数个数、类型或顺序不同),则这些函数就被称为重载函数。重载与函数的返回类型无关。

特点

  • 函数名相同。

  • 参数列表不同(包括参数个数、类型或顺序)。

  • 允许存在于同一个类或同一个命名空间中。

  • 调用时根据参数列表选择对应的函数版本(编译时多态)。

2、重写(Overriding)

定义派生类函数覆盖基类函数。

特点

  • 基类函数必须是虚函数。

  • 派生类函数与基类函数在名称、参数列表和返回类型上必须完全匹配。

  • 允许通过基类指针或引用来调用派生类中的函数(运行时多态)。

3、重定义(Hiding)

定义:也称为函数隐藏,当派生类中的函数与基类中的函数同名时,如果这两个函数的参数列表不同,或者基类函数没有声明为虚函数,即使参数列表相同,派生类中的函数也会隐藏基类中的同名函数。

特点

  • 派生类函数与基类函数同名。

  • 参数列表可能相同或不同。

  • 如果基类函数没有声明为虚函数,即使参数列表相同,基类函数也会被隐藏。

  • 隐藏与多态无关,它发生在编译时,编译器根据对象的静态类型(即编译时类型)来确定调用哪个函数。

总结

  • 重载是编译时多态的一种体现,允许在同一作用域内定义多个同名但参数列表不同的函数。

  • 重写是运行时多态的基础,通过基类指针或引用来调用派生类中的函数,前提是基类函数必须是虚函数。

  • 重定义(隐藏)可能导致意外的行为,特别是当基类函数被设计为通过基类指针调用时。它发生在编译时,且不受基类函数是否为虚函数的影响。

相关推荐
A-超2 小时前
vue3展示pag格式动态图
笔记
u0101526583 小时前
STM32F103C8T6学习笔记2--LED流水灯与蜂鸣器
笔记·stm32·学习
weixin_518285053 小时前
深度学习笔记10-多分类
人工智能·笔记·深度学习
丘狸尾3 小时前
ubuntu【桌面】 配置NAT模式固定IP
笔记
王俊山IT3 小时前
C++学习笔记----10、模块、头文件及各种主题(二)---- 预处理指令
开发语言·c++·笔记·学习
慕卿扬3 小时前
基于python的机器学习(二)—— 使用Scikit-learn库
笔记·python·学习·机器学习·scikit-learn
齐 飞5 小时前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
flying robot5 小时前
Go结构体(struct)
笔记
王俊山IT6 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(1)
开发语言·c++·笔记·学习
RationalDysaniaer6 小时前
Gin入门笔记
笔记·gin