C++语法[核心](三)

C++语法(三)

程序的内存模型

C++程序在执行时,内存大致分为四个区域:
代码区:存放函数体的二进制代码,由操作系统进行管理的。
全局区:存放全局变量和静态变量以及常量。
栈区:由编译器自动分配释放,存放函数的参数值、局部变量等。
堆区:由程序员分配和释放,若程序员不释放,程序结束操作系统回收。

内存四区的意义
不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活编程。

代码区和全局区都是在程序运行之前,栈区和堆区是在程序运行之后

内存四区

代码区

  • 存放CPU执行的机器指令
  • 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码。
  • 代码区是只读的,使其只读的原因是防止程序意外的修改了它的指令。

全局区

  • 存放全局变量和静态变量
  • 全局区还包含常量区:字符串常量和其他常量也存放在此
  • 全局区的数据在程序结束之后由操作系统释放

全局变量:
静态变量:普通变量前面加 static 关键字的都是静态变量
常量:

  • 字符串常量:带双引号(“”)的都是字符串常量
  • const修饰的变量:const修饰的全局变量和局部变量

栈区

局部变量:函数体内的定义的变量都叫局部变量。
Tips:不要返回局部变量的地址,栈区开辟的数据由编译器分配和自动释放。
栈区存放 形参数据和局部变量

堆区

在C++中主要利用new在堆区开辟内存
堆区开辟的数据,由程序员手动开辟、手动释放,释放利用关键字 delete
语法:new 数据类型
new创建的数据,会返回该数据对应的类型的指针。

int * func()
{
	// 指针本身就是局部变量,指针保存在栈上
	int *p = new int(10);	// 利用new关键字将数据开辟到堆区
	return p;
}
void func1()
{
	int * arr = new int[10]; // 开辟拥有10个元素的数组,返回数组的首地址
	for(int i; i < 10; i++)
	{
		arr[i] = i+100;
	}
	// 释放数组用delete[]
	delete[] arr;
}
int main()
{
	int *p = func();	// 在堆区开辟数据
	cout << *p << endl; // 可以正常打印
	delete p;	// 释放数据
	cout << *p << endl; // 不可以正常打印,会引发异常
}

C++中的引用

引用的基本使用

作用:给变量起别名
语法:数据类型 &别名 = 原名

引用的注意事项:

  • 引用必须初始化
  • 引用在初始化之后,不可以改变
int a = 10;
int &b = a;		// int &b; 是错误的
int c = 20;
b = c;			// 是赋值操作,不是更改引用。
//int &b = c;		//是错误的,引用初始化后,不可以改变。编译时会报错

引用做函数参数

作用:函数传参数时,可以利用引用的技术让形参修饰实参。
优点:可以简化指针修改实参。

// 引用传递
void func(int &a, int &b)
{
	int tmp = a;
	a = b;
	b = tmp;
}
int main()
{
	int a = 10, b = 20;
	func(a, b);
	cout << "a的值: " << a << endl; 	// a = 20
	cout << "b的值: " << b << endl;	// b = 10
}

引用做函数的返回值

作用:引用可以作为函数的返回值存在的
tips:不要返回局部变量引用
用法:函数调用作为左值

int& func1()
{
	static int a = 10;
	return a;
}
int mian()
{
	int &ref = func1();
}

引用的本质

引用的本质在C++内部实现是一个指针常量
指针常量是指针不可更改,这也是引用不可更改的原因。

int a = 10;
int *ref = a;	// 自动转换为int * const ref = a;
ref = 20; 		// 编译器内部发现ref是引用,自动转换为 *ref = 20;

常量引用

作用:常量引用主要用来修饰形参,防止误操作。
在函数参数列表中,可以加const来修饰形参,防止形参改变实参。

void func(const int &a)
{
	cout << a << endl;
}
int main()
{
	int &ref = 10; //这是错误语句,因为引用本身需要一个合法的内存空间
	const int &ref = 10; //这是正确的,编译器优化代码:int tmp = 10; const int &ref = tmp;
}

函数高级

形参:函数声明时定义的参数,用于接收调用函数时传入的实参。形参只存在于函数定义中,不能在函数外部使用。
形参变量只有在被调用时才分配内存单元,调用结束释放所分配的内存单元。
实参:调用函数时传递给函数的参数,实参的值会被传递给函数的形参。实参出现在主调函数中,进入被调函数后,实参就不能够用了。

每次调用函数时,都会重新创建该函数所有的形参,此时所传递的实参将会初始化对应的形参。

函数的默认参数、占位参数

函数的默认参数

语法:返回值类型 函数名(参数=数值,…) {}

  • 默认参数放在参数列表的最后,即默认参数之后的参数都得是默认参数
  • 如果函数声明有默认参数,那么函数定义就不能有默认参数
int func(int a= 0, int b = 5)
{
	return a+b;
}
int main()
{
	cout << func() << endl;		// 输出 5
	cout << func(2) << endl;	// 输出 7
	cout << func(2, 9) << endl;	// 输出 11
}

函数的占位参数

c++中函数的形参列表中可以有占位参数,可以用来占位,调用函数时必须填补该位置。
语法:返回值类型 函数名(数据类型) {}

void func1(int a, int)
{
	cout << "func1" << endl;
}
void func2(int a, int  = 20)	// 占位参数可以有默认参数
{
	cout << "func2" << endl;
}
int main()
{
	int a = 10, b = 20;
	func1(a, b);	
	func2(a);
}

函数的重载

作用:函数名可以相同,提高复用性。
函数重载满足的条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型个数不同顺序不同

tips: 函数的返回值不可以作为函数重载的条件

void func()
{
	cout << "func()" << endl;
}
void func(int a)	// 参数个数不同
{
	cout << "func(int a)" << endl;
}
void func(double a)
{
	cout << "func(float a)" << endl;	// 参数类型不同
}
void func(int a, double b)
{
	cout << "func(int a, float b)" << endl;
}
void func(double b, int a)
{
	cout << "func(float b, int a)" << endl;
}

int main()
{
	func(10);		// func(int a)
	func(3.14);		// func(double a)
	func(103.14// func(int a, double b)
	func(3.14, 10)	// func(double b, int a)
}

引用作为重载的条件

void func(int &a)
{
	cout << "func(int &a)" << endl;
}
void func(const int &a)
{
	cout << "func(const int &a)" << endl;
}

int main()
{
	int a = 10;
	func(a);	// 会调用 func(int &a)
	func(10);	// 会调用 func(const int &a)
	/* 因为对于func(10),如果走func(int &a),相当于:int &a = 10, 这是不合法的;如果走func(const int &a),相当于:const int &a = 10, 这是合法的*/
}

函数重载遇到默认参数
尽量避免此情况

类和对象

C++面向对象的三大特性:封装、继承、多态(也是所有面向对象语言的三大特性)
面向对象语言认为万事万物皆为对象,对象有属性和行为。
C++、python、java都是面向对象语言。

封装

封装的意义

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制

语法:class 类名{ 访问权限属性/行为 };

class Student
{
public:
    string names;
    int stu_num;
    
    string setName(string name)
    {
        names = name;
        return names;
    }
    int setNum(int num)
    {
        stu_num = num;
        return stu_num;
    }
    void showStudent()
    {
        cout << "name: " << names << "\tstu_num: " << stu_num << endl;
    }

int main()
{
    Student s1;
    s1.names = "张三";
    s1.stu_num = 7382;
    s1.showStudent();
    cout << "--------------" << endl;
    s1.setName("李四");
    s1.setNum(88);
    s1.showStudent();
}

访问权限有三种:

  • public 公共权限 (类内可以访问,类外也可以访问)
  • protected 保护权限 (类内可以访问,类外不可以访问;子类可以访问父类的保护内容)
  • private 私有权限 (类内可以访问,类外不可以访问;子类不可以访问父类的私有内容)

struct与class的区别

在C++中struct和class唯一区别就在于默认的访问权限不同.
区别:
struct 默认权限为公开
class 默认权限为私有

成员属性设置为私有

优点:

  1. 将所有成员设置为私有,可以自己控制读写权限
  2. 对于写权限,可以检测数据的有效性

案例1

立方体类:计算立方体的面积和体积,并通过全局函数和成员函数判断两个立方体是否一样。

#include <iostream>
#include <string>

using namespace std;

class Cube
{
private:
    float m_L=3, m_W=4, m_H=2;
public:
    void setH(float h)  //设置高
    {
        m_H = h;
    }   
    void setL(float l)  //设置长
    {
        m_L = l;
    }
    void setW(float w)  //设置宽
    {
        m_W = w;
    }
    float getH()        //获取高
    {
        return m_H;
    }
    float getL()        //获取长
    {
        return m_L;
    }
    float getW()        //获取宽
    {
        return m_W; 
    }
    float surface_area()    //获取面积
    {
        return getH() * getL() *2 + getL() * getW() *2 + getH() * getW() *2;
    }

    float volume()          //获取体积
    {
        return getH() * getL() * getW();
    }
    // 利用成员函数判断两个立方体是否一样
    bool isSame(Cube &c)
    {
        if (m_H == c.getH() && m_L == c.getL() && m_W == c.getW())
        {
            return true;
        }
        return false;
        
    }
};

bool isEqual(Cube &c1, Cube &c2)
{
    if (c1.getH() == c2.getH() && c1.getL() == c2.getL() && c1.getW() == c2.getW())
    {
        return true;
    }
    return false;
}


int main()
{
    Cube C1;
    C1.setH(10);
    C1.setL(10);
    C1.setW(10);
    cout << "表面积1:" << C1.surface_area() << "  体积1:" << C1.volume() << endl;
    Cube C2;
    C2.setH(10);
    C2.setL(10);
    C2.setW(100);
    cout << "表面积2:" << C2.surface_area() << "  体积2:" << C2.volume() << endl; 
    int ret1 = isEqual(C1, C2);
    if (ret1)
    {
        cout << "全局函数判断: C1和C2一样" << endl;
    }
    else
    {
        cout << "全局函数判断: C1和C2不一样" << endl;
    }
    
    int ret2 = C2.isSame(C1);
    if (ret2)
    {
        cout << "成员函数判断: C1和C2一样" << endl;
    }
    else
    {
        cout << "成员函数判断: C1和C2不一样" << endl;
    }
}

案例2

点和圆的关系:设计一个圆形类(Circle)和一个点类(Point),计算点和圆的关系。

#include <iostream>
#include <string>

using namespace std;

class Point
{
private:
    int x, y;

public:
    void setPx(int x_)
    { x = x_;}

    void setPy(int y_)
    { y = y_;}

    int getPx()
    { return x; }

    int getPy()
    { return y; }
};

class Circle
{
private:
    int c_R;
    Point centre;

public:
    void setRadius(int rr)
    { c_R = rr; }

    int getRadius()
    { return c_R; }

    void setCentre(Point p)
    { centre = p; }

    Point getCenter()
    { return centre;}
}; 
//判断点与圆的关系
void relation(Point &p, Circle &cc)
{
    // 计算两点之间的距离
    int distance = (cc.getCenter().getPx()-p.getPx())*(cc.getCenter().getPx()-p.getPx()) +
                   (cc.getCenter().getPy()-p.getPy())*(cc.getCenter().getPy()-p.getPy());
    int rDistance = cc.getRadius()*cc.getRadius();
    if (distance == rDistance)
    {
        cout << "点在圆上" << endl;
    }
    else if (distance < rDistance)
    {
        cout << "点在圆内" << endl;
    }
    else
    {
        cout << "点在圆外" << endl;
    }
}

int main()
{
    Circle c1;
    c1.setRadius(5);
    Point center;
    center.setPx(5);
    center.setPy(4);
    c1.setCentre(center);
    Point p1;
    p1.setPx(8);
    p1.setPy(8);

    relation(p1, c1);
}

对象的初始化和清理

构造函数和析构函数

对象的初始化和清理是两个非常重要的安全问题

  • 一个对象或变量没有初始状态,对其使用后果是未知的
  • 使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

C++使用构造函数和析构函数解决了以上问题,这两个函数会被编译器自动调用,完成对象初始化和清理工作。
对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供;只不过编译器提供的构造函数和析构函数是空实现

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数语法类名() {}

  • 构造函数没有返回值也不写void
  • 函数名称和类名相同
  • 构造函数可以有参数,因此可以发生重载
  • 程序在调用对象时会自动调用构造函数,无需手动调用,而且只会调用一次

析构函数语法~类名() {}

  • 析构函数没有返回值也不写void
  • 函数名称和类名相同,在名称前面加上“~”符号
  • 析构函数不可以有参数,所以不可以发生重载
  • 程序在对象销毁之前会自动调用析构函数,无需手动调用,而且只会调用一次

构造函数的分类及调用

两种分类方式:

  • 按参数分类为:有参构造和无参构造
  • 按类型分类为:普通构造和拷贝构造

三种调用方式:括号法、显示法、隐式转换法

#include <iostream>
using namespace std;

class Point
{
private:
    /* data */
public:
	int age = 8;
    // 构造函数
    Point(int a)
    {
    	int num = a;
        cout << "有参构造函数, 参数:" << num << endl;
    }
    Point()
    {
		cout << "无参构造函数" << endl;
	}
	Point(const Point &p)
	{
		// 将传入的对象的所有属性拷贝至此对象上
		int age = p.age;	
		cout << "拷贝构造函数,参数:" << age << endl;
	}
    // 析构函数
    ~Point()
    {
        cout << "析构函数" << endl;
    }
};

int main()
{
	/* 括号法 */
	Point p1;		//无参(默认)构造函数调用【不加()】
	Point p2(10);	//有参构造函数调用
	Point p3(p2);	//拷贝构造函数调用
	/* 显示法 */
	Point p4;
    Point p5 = Point(20);	//【有参构造】
    Point p6 = Point(p5); 	//【拷贝构造】
    Point(20); //匿名对象。特点:当前行执行结束后,系统会立即回收掉匿名对象
    // 不要利用拷贝构造函数 初始化匿名对象,编译器会认为 Point(p3)===Point p3
    /* 隐式转换法 */
    Point p7 = 10; //相当于Point p7 = Point(10); 【有参构造】
    Point p8 = p7; //相当于Point p8 = Point(p7); 【拷贝构造】
}

拷贝构造函数的调用时机

通常有三种情况:

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象

示例类:Person

#include <iostream>
using namespace std;

class Person
{
private:
    /* data */
public:
    int m_Age = 10;
    Person()	// 普通构造函数
    {
        cout << "无参构造函数" << endl;
    }
    // 有参构造函数
    Person(int a)
    {
        m_Age = a;
        cout << "有参构造函数" << m_Age << endl;
    }
    // 拷贝构造函数
    Person(const Person &p)
    {
        m_Age = p.m_Age;
        cout << "拷贝构造函数" << m_Age << endl;
    }
    // 析构函数
    ~Person()
    {
        cout << "析构函数" << endl;
    }
};
#include <iostream>
using namespace std;
/* 使用“示例类:Person”*/
// 1.使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
    Person p1(20);
    Person p2(p1);
    cout << "p2的年龄: " << p2.m_Age << endl;
}
// 2.值传递的方式给函数参数传值
void doWork(Person p)
{

}
void test02()
{
    Person p;
    doWork(p);
}
// 3.值方式返回局部对象
Person work()
{
    Person p;
    return p;
}
void test03()
{
    Person p = work();
}

int main()
{
    test01();
    test02();
    test03();
}

构造函数调用规则

默认情况下,C++编译器至少给一个类添加3个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,C++不再提供其他构造函数
#include <iostream>
using namespace std;
class Person
{
private:
    /* data */
public:
    int m_Age = 10;
    // 有参构造函数
    Person(int a)
    {
        m_Age = a;
        cout << "有参构造函数" << m_Age << endl;
    }
    // 析构函数
    ~Person()
    {
        cout << "析构函数" << endl;
    }
};

void test01()
{
	Person p(18);
	Person p1(p);
	cout << "" << p1.m_Age << endl;
}
int main()
{
	test01();
}
/* 输出结果:
有参构造函数18
p1的年龄:18
析构函数
析构函数
*/

深拷贝与浅拷贝

浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作

利用编译器提供的拷贝构造函数,会做浅拷贝操作。

#include <iostream>
using namespace std;

class Person
{
public:
    int m_Age;
    int *m_height;
    Person()
    {
        cout << "无参构造函数" << endl;
    }
    // 构造函数
    Person(int a, int height)
    {
        m_Age = a;
        m_height = new int(height);
        cout << "有参构造函数" << endl;
    }
    // 拷贝构造函数
    Person(const Person &p)
    {
        cout << "拷贝构造函数" << endl;
        // 如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
        m_Age = p.m_Age;
        // m_height = p.m_height;  编译器默认实现的就是这行代码
        m_height = new int(*p.m_height);  // 深拷贝操作
    }
    // 析构函数
    ~Person()
    {
        cout << "析构函数" << endl;
        // 释放在堆区开辟的空间
        if (m_height != NULL)
        {
            delete m_height;
        }  
    }
};
void test01()
{
    Person p1(20, 170);
    cout << "p1的年龄: " << p1.m_Age << " p1的身高: " << *p1.m_height << endl;
    Person p2(p1);
    cout << "p2的年龄: " << p2.m_Age << " p2的身高: " << *p2.m_height << endl;   
}
int main()
{
    test01();
}
/* 输出结果
有参构造函数
p1的年龄: 20 p1的身高: 170
拷贝构造函数
p2的年龄: 20 p2的身高: 170
析构函数
析构函数
*/

总结:如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题(问题:堆区的内存重复释放)。

初始化列表

作用:C++提供了初始化列表语法,用来初始化属性
语法:构造函数()属性1(值1)属性2(值2){}

#include <iostream>
using namespace std;

class Person
{
public:
    int m_A, m_B;
    // 传统初始化操作
    // Person(int a, int b)
    // {
    //     m_A = a;
    //     m_B = b;
    // }
    // 初始化列表初始化属性
    Person(int a, int b): m_A(a), m_B(b)
    {
        /* code or null*/
    }
};
void test01()
{
    // Person p(30,60);
    Person p(50, 20);
    cout << p.m_A << endl;
}

int main()
{
    test01();
}

类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为“对象成员”。

#include <iostream>
#include <string>
using namespace std;

class Phone
{
public:
    string m_PName;

    Phone(string pName)
    {
        m_PName = pName;
        cout << "Phone的构造函数调用" << endl;
    }
    ~Phone()
    {
        cout << "Phone的析构函数调用" << endl;
    }
};

class Person
{
public:
    string m_Name;
    Phone m_Phone;	//	对象成员

    Person(string name, string pName): m_Name(name), m_Phone(pName)
    {
        cout << "Person的构造函数调用" << endl;
    }
    ~Person()
    {
        cout << "Person的析构函数调用" << endl;
    }
};
void test01()
{
    Person p("李四", "三星");
    cout << p.m_Name << "拿着" << p.m_Phone.m_PName << "手机." << endl;
}

int main()
{
    test01();
}
/* 输出结果
Phone的构造函数调用
Person的构造函数调用
李四拿着三星手机.
Person的析构函数调用
Phone的析构函数调用
*/

总结:
构造函数的调用顺序:先调用对象成员(Phone)的构造函数,再调用自身(Person)的构造函数。
析构函数的调用顺序:先调用自身(Person)的析构函数,再调用对象成员(Phone)的析构函数。[与构造函数相反]

静态成员

在成员变量和成员函数之前加关键字static,称之为静态成员
静态成员变量:

  • 所有对象共享同一份数据
  • 在编译阶段分配内存
  • 类内声明,类外初始化

静态成员函数:

  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
#include <iostream>
#include <string>

using namespace std;

class Person
{
// 类外不可以访问私有静态成员变量
private:
    static int m_B;
    static void func2()
    {
        cout << "static void func2调用" << endl;
    }

public:
    static int m_A;     // 类内声明
    int m_C;
    static void func()
    {
        m_A = 100;
        // m_C = 300;  // 静态成员函数不能访问非静态成员变量
        cout << "static void func调用" << endl;
    }
};

int Person::m_A = 10;   // 类外初始化
int Person::m_B = 20;

void test01()
{
    /* 静态成员变量不属于某个对象上,所有对象都共享同一份数据,因此静态成员变量有两种访问方式*/
    // 1.通过对象进行访问静态成员变量
    Person p;
    cout << "p.m_A: " << p.m_A << endl;             // p.m_A=10
    Person p2;
    p2.m_A = 20;
    cout << "修改过后 p.m_A: " << p.m_A << endl;     // p.m_A=20
    // 2.通过类名访问静态成员变量
    cout << "通过类名访问静态成员变量:" << Person::m_A << endl;
    
    // cout << "访问类私有成员" << Person::m_B << endl;    // 会提示"m_B"不可访问
}
void test02()
{
    //1.通过对象进行访问静态成员函数
    Person p;
    p.func();

    //2.通过类名访问静态成员函数
    Person::func();
    // Person::func2();    // 类外不能访问私有静态成员函数
}

int main()
{
    test01();
    test02();
}

C++对象模型和this指针

成员变量和成员函数分开存储

在C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上

C++编译器会为每个空对象分配1个字节空间,是为了区分空对象占内存的位置。每个空对象都有个独一无二的内存地址。

#include <iostream>
using namespace std;

class Kong
{
};
class Person
{
    int m_a;		// 非静态成员变量
};
class Phone
{
    static int m_c;		// 静态成员变量
    static void func(){}	// 静态成员函数
};
int Phone::m_c = 10;

class Person2
{
    int m_a;		// 非静态成员变量
    void func(){}	// 非静态成员函数
};
void test00()
{
    Kong k;
    cout << "size of k: " << sizeof(k) << endl;     // size of k: 1
}
void test01()
{
    Person p;
    cout << "size of p: " << sizeof(p) << endl;     // size of p: 4, 非静态成员属于对象上
}
void test02()
{
    Phone pe;
    cout << "size of pe: " << sizeof(pe) << endl;    // size of pe: 1, 静态成员和函数不属于类的对象上,所以不占类对象内存
}
void test03()
{
	Person2 pp;
	cout << "size of pp: " << sizeof(pp) << endl;		// size of pp: 4, 成员变量和成员函数是分开存储的
}

int main()
{
    test00();
    test01();
    test02();
    test03();
}

this指针概念

this指针指向被调用的成员函数所属的对象
this指针是隐含每一个非静态成员函数内的一种指针
this指针不需要定义,直接使用即可
this指针的本质:是指针常量,指针的指向不可以更改

this指针的用途:

  • 当形参和成员变量同名时,可用this指针来区分【解决名称冲突】
  • 在类的非静态成员函数中返回对象本身,可使用return *this
#include <iostream>
using namespace std;

class Person
{
public:
    int age;
    Person(int age)
    {
        this -> age = age;
        // age = age;
    }
    // 返回引用,不是返回值。返回值的话,每次调用都会创建一个新的对象
    Person& PersonAddAge(Person &p)
    {
        this -> age += p.age;
        // this指向p3的指针,而*this指向的就是p3这个对象本身
        return *this;
    }
};

// 1.解决名称冲突问题
void test01()
{
    Person p1(18);
    cout << "p1的年龄: " << p1.age << endl;
}

// 2.返回对象本身用*this
void test02()
{
    Person p2(10);
    Person p3(20);
    p3.PersonAddAge(p2);
    cout << "p3的年龄: " << p3.age << endl;         // 30
    // 链式编程思想
    p3.PersonAddAge(p2).PersonAddAge(p2).PersonAddAge(p2);
    cout << "now, p3的年龄: " << p3.age << endl;    // 60
}

int main()
{
    test02();
}

空指针访问成员函数

C++中空指针也可以调用成员函数,但要注意有没有用到this指针;如果用到this指针,需要加以判断保证代码的健壮性。

#include <iostream>
using namespace std;

class Person
{
public:
    int m_Age;

    void showPersonName()
    {
        cout << "this is Person class" << endl;
    }
    void showPersonAge()
    {
        if (this == NULL)
        {
            return;
        }
        // 以上代码是为了避免出现空指针的
        cout << "age = " << m_Age << endl;
    }
};

void test01()
{
    Person *p = NULL;
    p->showPersonName();
    p->showPersonAge();     //如果不加if语句判断,会报错,因为传入的指针为NULL
}

int main()
{
    test01();
}

const修饰成员函数

常函数

  • 成员函数后 加const 我们称这个函数为常函数
  • 常函数内不可以修改成员属性
  • 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象

  • 声明对象前加const 称该对象为常对象
  • 常对象只能调用常函数

Tips:在成员函数后加const,修饰的是this指针指向,让指针指向的值也不可以更改

#include <iostream>
using namespace std;

class Person
{
public:
    int m_A;
    mutable int m_B;    //特殊变量,加mutable关键字,即使在常函数中也可以修改这个变量

    Person()
    {

    }
    void showPerson1()          //相当于 Person * const this
    {
        this->m_A = 10;
    }
    //在成员函数后加const,修饰的是this指针指向,让指针指向的值也不可以更改
    void showPerson2() const    //相当于 const Person * const this  
    {
        // this->m_Age = 10;       //会报错
        this->m_B = 20;
    }
};

void test01()
{
    Person *p = NULL;
}

void test02()
{
    const Person p2;     // 常对象
    // p2.m_A = 100;    // 会报错,不可修改
    p2.m_B = 200;       // m_B是特殊值,在常对象下可以修改
    p2.showPerson2();   // 常对象只能调用常函数
    // p2.showPerson1();   // 会报错,常对象不能调用非常函数
}

int main()
{
    test01();
    test02();
}

友元

友元的作用就是让一个函数或类 访问另一个类的私有成员
友元的关键字是 friend
友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

全局函数做友元

#include <iostream>
#include <string>
/* 全局函数做友元 */
using namespace std;

class Building
{
    // 声明goodGay全局函数为友元
    friend void goodGay(Building &build);	// 全局函数做友元
private:
    string m_BedRoom;
public:
    string m_SittingRoom;

    Building()
    {
        m_SittingRoom = "客厅";
        m_BedRoom = "卧室";
    }
};
//全局函数
void goodGay(Building &build)
{
    cout << "好基友全局函数 访问:" << build.m_SittingRoom << endl;
    cout << "好基友全局函数 访问:" << build.m_BedRoom << endl;     // 不声明goodGay为友元的情况下,此行代码会报错
}
void test01()
{
    Building build;
    goodGay(build);
}

int main()
{
    test01();
}

类做友元

#include <iostream>
#include <string>
/* 类做友元 */
using namespace std;

class Building
{
    friend class GoodGay;	// 类做友元
private:
    string m_BedRoom;
public:
    string m_SittingRoom;

    Building()
    {
        m_SittingRoom = "客厅";
        m_BedRoom = "卧室";
    }
};

class GoodGay
{
public:
    Building *building;

    GoodGay()
    {
        building = new Building;
    }
    // void visit();   // 此函数访问Building中的属性
    void visit()
    {
       cout << "好基友全局函数 访问:" << building->m_SittingRoom << endl;  
       cout << "好基友全局函数 访问:" << building->m_BedRoom << endl; 
    }
};
void test02()
{
    GoodGay gg;
    gg.visit();
}
int main()
{
    test02();
}

成员函数做友元

#include <iostream>
#include <string>

using namespace std;
class Building;
class goodGay
{
private:
    Building *building;
public:
    goodGay();
    void visit();    // 此函数可以访问Building中的私有属性
    void visit2();   // 此函数不可以访问Building中的私有属性
};
class Building
{
    friend void goodGay::visit();           // 成员函数做友元
private:
    string m_BedRoom;
public:
    string m_SittingRoom;
    Building();
};

Building::Building()
{
    this->m_SittingRoom = "客厅";
    this->m_BedRoom = "卧室"; 
}
goodGay::goodGay()
{
    building = new Building;
}
void goodGay::visit()
{
    cout << "visit函数 访问:" << building->m_SittingRoom << endl;  
    cout << "visit函数 访问:" << building->m_BedRoom << endl; 
}
void goodGay::visit2()
{
    cout << "visit2函数 访问:" << building->m_SittingRoom << endl;
    // cout << "visit2函数 访问:" << building->m_BedRoom << endl;  // 不能访问私有属性
}

void test01()
{
    goodGay gg;
    gg.visit();
    gg.visit2();
}

int main()
{
    test01();
}

C++运算符重载

运算符重载概念:对已有的运算符进行重新定义,赋予其另一种功能,以适应不同的数据类型。

加号运算符重载

作用:实现两个自定义数据类型相加的运算

  1. 成员函数重载加号
#include <iostream>
using namespace std;

class Person
{
public:
    int m_A;
    int m_B;
    Person operator+(Person &p)
    {
        Person temp;
        temp.m_A = this->m_A + p.m_A;
        temp.m_B = this->m_B + p.m_B;
        return temp;
    }
};
void test01()
{
    Person p1;
    p1.m_A = 10;
    p1.m_B = 20;
    Person p2;
    p2.m_A = 10;
    p2.m_B = 20;

    Person p3 = p1 + p2;	// 成员函数重载本质调用: Person p3 = p1.operator+(p2);[与此行代码等效]
    cout << "p3.m_A:" << p3.m_A << " p3.m_B: " << p3.m_B << endl;
}

int main()
{
    test01();
}
  1. 全局函数重载加号
#include <iostream>
using namespace std;

class Person
{
public:
    int m_A;
    int m_B;
};

Person operator+(Person &p1, Person &p2)
{
    Person temp;
    temp.m_A = p1.m_A + p2.m_A;
    temp.m_B = p1.m_B + p2.m_B;
    return temp;
}
// 函数重载运算符版本
Person operator+(Person &p1, int num)
{
    Person temp;
    temp.m_A = p1.m_A + num;
    temp.m_B = p1.m_B + num;
    return temp;
}
void test01()
{
    Person p1;
    p1.m_A = 10;
    p1.m_B = 20;
    Person p2;
    p2.m_A = 10;
    p2.m_B = 20;
    
    Person p3 = p1 + p2; // 全局函数重载本质调用:Person p3 = operator+(p1, p2); [与此行代码等效]
    cout << "p3.m_A:" << p3.m_A << " p3.m_B: " << p3.m_B << endl;
    Person p4 = p1 + 5;
    cout << "p4.m_A:" << p3.m_A << " p4.m_B: " << p3.m_B << endl; // 输出结果:p4.m_A: 15  p4.m_B: 25
}

int main()
{
    test01();
}

总结

  1. 内置的数据类型的表达式的运算符是不可能改变的。
  2. 不要滥用运算符重载

左移运算符重载

作用:可以输出自定义数据类型
只能利用全局函数重载左移运算符

class Person
{
public:
    int m_A;
    int m_B;
};
// 只能利用全局函数重载左移运算符
// “ostream&” 链式编程 
ostream& operator<<(ostream &cout, Person &p) //本质:operator<<(cout, p)  简化为 cout << p
{
    cout << "m_A:" << p.m_A << " m_B: " << p.m_B << endl;
    return cout;	// 链式编程
} 
int main()
{
	Person p;
	p.m_A = 10;
	p.m_B = 20;
	cout << p << endl;	// 输出为 m_A:10 m_B:20
}

递增运算符重载 [与递减预算符雷同:++改成- -]

作用:通过重载递增运算符,实现自己的整型数据

#include <iostream>
using namespace std;

class MyInteger
{
    friend ostream& operator<<(ostream &cout, const MyInteger &myData);
private:
    int m_Num;
public:
    MyInteger()
    {
        m_Num = 0;
    }
    // 重载前置++运算符 [前置:先自增,再参与运算]
    MyInteger& operator++()     //返回引用而不是返回值的原因:一直对一个数据进行递增操作  
    {
        ++m_Num;        // 先进行++运算
        return *this;   // 再将自身做返回
    }
    // 重载后置++运算符 [后置:先参与运算,再自增]
    MyInteger operator++(int)  // int代表占位参数,用于区分前置和后置递增
    {   
        MyInteger temp = *this; // 先 记录当时结果
        m_Num++;                // 后 递增
        return temp;            // 最后将记录结果返回
    }
};
// 重载<<运算符
ostream& operator<<(ostream &out, const MyInteger &myData) // 加“const”的原因:C++不允许非常量的临时引用作为参数
{
    out << myData.m_Num << endl;
    return out;
}

void test01()
{
    MyInteger myint;
    cout << myint << endl;
    cout << ++(++myint) << endl;
    cout << myint << endl;
}
void test02()
{
    MyInteger myInt;
    cout << myInt << endl;	// 输出0
    cout << myInt++ << endl;// 输出1
    cout << myInt << endl;	// 输出1
    myInt++;
    cout << myInt << endl;	// 输出2
}

int main()
{
    test01();
    cout << "------" << endl;
    test02();
}

赋值运算符重载

C++编译器至少给一个类添加4个函数:

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行值拷贝
  • 赋值运算符 operator=,对属性进行值拷贝

如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。

#include <iostream>
using namespace std;

class Person
{
public:
    int *m_Age;
    Person(int age)
    {
        m_Age = new int(age);
    }
    ~Person()
    {
        if (m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        } 
    }
    // 重载 赋值操作符
    Person& operator=(Person &p)
    {
        // m_Age = p.m_Age;     //编译器是提供浅拷贝
        //应该先判断是否有属性在堆区,如果有先释放干净,然后再深拷贝
        if(m_Age != NULL)
        {
            delete m_Age;
            m_Age = NULL;
        }
        // 深拷贝
        m_Age = new int(*p.m_Age);
        return *this;
    }
};

void test01()
{
    Person p1(18);
    Person p2(20);
    Person p3(28);
    p3 = p2 = p1;        // 赋值操作
    cout << "p1的年龄: " << *p1.m_Age << endl;
    cout << "p2的年龄: " << *p2.m_Age << endl;
    cout << "p3的年龄: " << *p3.m_Age << endl;
}

int main()
{
    test01();
}

关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
    string m_Name;
    int m_Age;

    Person(string name, int age)
    {
        m_Name = name;
        m_Age = age;
    }
    // 重载 == 运算符
    bool operator==(Person &p)     // , Person &p2
    {
        if (this->m_Age == p.m_Age && this->m_Name == p.m_Name)
        {
            return true;
        }
        return false;  
    }
    // 重载 != 运算符
    bool operator!=(Person &p)
    {
        if (this->m_Age != p.m_Age || this->m_Name != p.m_Name)
        {
            return true;
        }
        return false; 
    }
};

void test01()
{
    Person p1("tom", 18);
    Person p2("tim", 18);
    if (p1 == p2)
    {
        cout << "p1与p2相等" << endl;
    }
    else
    {
        cout << "p1与p2不相等" << endl;
    }

    if (p1 != p2)
    {
        cout << "p1与p2不相等" << endl;
    }
    else
    {
        cout << "p1与p2相等" << endl;
    }   
}

int main()
{
    test01();
}

函数调用运算符重载

  • 函数调用运算符"()"也可以重载
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数
  • 仿函数没有固定写法,非常灵活
#include <iostream>
#include <string>
using namespace std;

class MyPrint
{
public:
    void operator()(string test)
    {
        cout << test << endl;
    }
};
class MyAdd
{
public:
    int operator()(int a, int b)
    {
        return a+b;
    }
};
void test01()
{
    MyPrint mp1;
    mp1("hello");
}
void test02()
{
    MyAdd myadd;
    int num = myadd(10,10);
    cout << num << endl;
}

int main()
{
    test01();
    test02();
}

继承

继承是面向对象三大特性之一

继承基本语法

语法:class 子类继承方式 父类
子类 也称为 派生类
父类 也称为 基类

#include <iostream>
#include <string>
using namespace std;

class BasePage
{
public:
    void header()
    {
        cout << "首页、公开课、登录、注册...(公共头部)" << endl;
    }
    void footer()
    {
        cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;
    }
    void left()
    {
        cout << "Java、Python、C++...(公共分类列表)" << endl;
    }
};

class Java:public BasePage
{
public:
    void content()
    {
        cout << "Java学科视频" << endl;
    }
};
class Python:public BasePage
{
public:
    void content()
    {
        cout << "Python学科视频" << endl;
    }
};

void test01()
{
    cout << "Java下载视频页面如下: " << endl;
    Java ja;
    ja.header();
    ja.footer();
    ja.left();
    ja.content();
    
    cout << "Python下载视频页面如下: " << endl;
    Python py;
    py.header();
    py.footer();
    py.left();
    py.content();
}

int main()
{
    test01();
}

继承方式

  • 公共继承
  • 保护继承
  • 私有继承

父类中的私有属性/成员,不可以被子类继承(无论哪种继承方式);
公共继承:public、protected权限,子类父类相同;
保护继承:父类的public、protected内容 -> 子类的protected内容
私有继承:父类的public、protected内容 -> 子类的private内容
三种继承方式

继承中的对象模型

父类中所有的非静态成员属性都会被子类继承下去。
父类中私有成员,是被编译器隐藏了,所以访问不到,但确实会被子类继承。

#include <iostream>
using namespace std;

class Base
{
public:
    int m_A;
protected:
    int m_B;
private:
    int m_C;
};

class Son:public Base
{
public:
    int m_D;
};

void test01()
{
    Son s1;
    cout << "size of Son = : " << sizeof(Son) << endl;	// 输出:16 (4*4)
}

int main()
{
    test01();
}

继承中的构造和析构顺序

子类继承父类,当子类创建对象时,也会调用父类的构造函数。
继承中构造的顺序:先构造父类再构造子类
继承中析构的顺序:先构造子类再构造父类(与构造顺序相反)

#include <iostream>
using namespace std;

class Base
{
public:
    Base()
    {
        cout << "Base构造函数!" << endl;
    }
    ~Base()
    {
        cout << "Base析构函数!" << endl;
    }
};

class Son:public Base
{
public:
    Son()
    {
        cout << "Son构造函数!" << endl;
    }
    ~Son()
    {
        cout << "Son析构函数!" << endl;
    }
};

void test01()
{
    Son s1;
}

int main()
{
    test01();
}
/* 输出结果:
Base构造函数!
Son构造函数!
Son析构函数!
Base析构函数! */

继承中同名成员的处理方式

当子类和父类中有同名的成员,如何通过子类对象访问到子类或父类中同名的数据呢?

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域
#include <iostream>
using namespace std;

class Base
{
public:
    int m_A;
    Base()
    {
        m_A = 100;
    }
    void func()
    {
        cout << "Base - func" << endl;
    }
};
class Son:public Base
{
public:
    int m_A;
    Son()
    {
        m_A = 200;
    }
    void func()
    {
        cout << "Son - func" << endl;
    }
};

void test01()
{
    Son s1;
    cout << "Son m_A: " << s1.m_A << endl;
    cout << "Base m_A: " << s1.Base::m_A << endl;
}
void test02()
{
    Son s2;
    s2.func();          // 直接调用 调用的是子类中的成员函数
    s2.Base::func();    // 父类中的成员函数
}

int main()
{
    test01();
    test02();
}

继承中同名静态成员处理方式

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员,直接访问即可
  • 访问父类同名成员,需要加作用域

只不过有两种访问方式:通过对象访问和通过类名访问

#include <iostream>
using namespace std;

class Base
{
public:
    static int m_A;
    static void func();
};
int Base::m_A = 100;
void Base::func()
{
    cout << "Base - static func()" << endl;
}

class Son:public Base
{
public:
    static int m_A;
    static void func();
};
int Son::m_A = 200;
void Son::func()
{
    cout << "Son - static func()" << endl;
}

void test01()
{
    cout << "*****通过对象访问参数*****" << endl;
    Son s1;
    cout << "Son static m_A: " << s1.m_A << endl;
    cout << "Base staic m_A: " << s1.Base::m_A << endl;
    cout << "*****通过类名访问参数*****" << endl;
    cout << "Son static m_A: " << Son::m_A << endl;
    // 第一个::代表通过类名方式访问,第二个::代表访问父类作用域下
    cout << "Base static m_A: " << Son::Base::m_A << endl;
}
void test02()
{
    cout << "*****通过对象访问函数*****" << endl;
    Son s1;
    s1.func();          // 子类中的func
    s1.Base::func();    // 父类中的func
    cout << "*****通过类名访问函数*****" << endl;
    Son::func();        // 子类中的func 
    // 第一个::代表通过类名方式访问,第二个::代表访问父类作用域下
    Son::Base::func();  // 父类中的func
}

int main()
{
    test01();
    test02();
}

多继承语法

C++允许一个类继承多个类
语法:class 子类继承方式 父类1继承方式 父类2,…
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承

#include <iostream>
using namespace std;

class Base1
{
public:
    int m_A;
    Base1()
    {
        m_A = 100;
    }
};

class Base2
{
public:
    int m_B, m_A;
    Base2()
    {
        m_B = 200;
        m_A = 10;
    }
};

class Son:public Base1, public Base2
{
public:
    int m_C, m_D;
    Son()
    {
        m_C = 300;
        m_D = 400;
    }
};

void test01()
{
    Son s;
    cout << "size of Son = " << sizeof(s) << endl;  // 输出:20字节
    cout << "Base1 m_A: " << s.Base1::m_A << endl;  // 输出:100
    cout << "Base2 m_A: " << s.Base2::m_A << endl;  // 输出:10
}

int main()
{
    test01();
}

菱形继承

菱形继承概念:
两个派生类(子类)继承同一个基类(父类),又有某个类同时继承这两个派生类,这种继承被称为菱形继承或钻石继承。
菱形继承典型案例:
典型的菱形继承问题
菱形继承带来的问题:

  1. 羊继承了动物的数据,驼继承了动物的数据,当羊驼要使用数据时,就会产生二义性。
  2. 羊驼继承了两份动物的数据,但这份数据我们只需要一份即可。

解决方法:
两个父类拥有相同数据时,需要加作用域用以区分。
利用虚继承解决菱形继承有两份数据的问题:

  • 继承方式之前加关键字“virtual”,变为虚基类;
  • 被继承的基类称为虚基类
#include <iostream>
using namespace std;

class Animals 
{
public:
    int m_Age;
};

class Sheep:virtual public Animals {};

class Tuo:virtual public Animals {};

class SheepTuo:public Sheep, public Tuo {};

void test01()
{
    SheepTuo st;
    st.Sheep::m_Age = 10;
    cout << st.m_Age << endl;	// 输出结果为:10
    st.Tuo::m_Age = 20;
    cout << st.m_Age << endl;   // 如果不加virtual关键字,此行代码会报错。输出结果为:20 [数据被共享了]
}

int main()
{
    test01();
}

虚继承原理:
SheepTuo类中使用virtual关键字将Animals类标记为虚基类。这将导致编译器在生成SheepTuo类的对象时,为它们分配一个指向Animals类的虚基类表的指针。这个虚基类表包含了Animals类的地址偏移量,使得SheepTuo类可以通过这个指针来访问Animals类的成员。
SheepTuo类继承SheepTuo类时,它会包含两个指向Animals类虚基类表的指针,这些指针指向同一个虚基类表,这个虚基类表中包含了Animals类的地址偏移量。这使得SheepTuo类可以通过这些指针来访问Animals类的成员,而不会出现数据重复和冗余的情况。

虚继承的原理是通过在派生类中维护一个指向虚基类表的指针,来实现对虚基类的间接访问,从而避免多重继承中的数据重复和冗余问题。

虚继承的实现需要在编译器中生成虚基类表,并在派生类中维护这个表的指针。由于虚继承需要额外的开销和复杂度,应该谨慎使用,只在必要的情况下才使用虚继承。

多态

多态的基本概念

多态是面向对象三大特性之一

多态分为两类:

  • 静态多态:函数重载、运算符重载属于静态多态,复用函数名
  • 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态的区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

动态多态满足的条件:

  1. 有继承关系
  2. 子类重写父类的虚函数

重写:函数返回值类型、函数名、参数列表 完全一致才称为重写

动态多态的使用:

  1. 父类的指针或引用,指向子类对象
#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void speak()            // 虚函数 (使函数地址在运行时绑定)
    {
        cout << "动物在说话" << endl;
    }
};

class Cat:public Animal
{
public:
    void speak()
    {
        cout << "猫在说话" << endl;
    }
};

class Dog:public Animal
{
public:
    void speak()
    {
        cout << "狗在说话" << endl;
    }
};

void doSpeak(Animal &animal)    // Animal &animal = cat or dog; 
{
    animal.speak();
}

void test01()
{
    Cat cat;
    doSpeak(cat);   // 如果不加virtual关键字,则输出:“动物在说话”[地址早绑定]。加了virtual关键字,输出:“猫在说话”[地址晚绑定]

    Dog dog;
    doSpeak(dog);   // 输出:“狗在说话”
}

int main()
{
    test01();
}

多态的原理剖析

C++中的多态性是通过虚函数(virtual functions)实现的。虚函数是在基类中声明的函数,可以在派生类中被重写
在C++中,虚函数使用virtual关键字来声明。使用虚函数时,如果一个指针或引用指向基类对象,但实际指向派生类对象,则在调用虚函数时将调用派生类中的版本。

在C++中,使用虚函数来实现多态性有两个关键点:

  1. 将基类函数声明为虚函数。
  2. 使用指向基类对象的指针或引用来调用虚函数。
#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void speak()  // 虚函数 (使函数地址在运行时绑定)
    {
        cout << "动物在说话" << endl;
    }
};

class Cat:public Animal
{
public:
    void speak()
    {
        cout << "猫在说话" << endl;
    }
};

class Dog:public Animal
{
public:
    void speak()
    {
        cout << "狗在说话" << endl;
    }
};
// 使用引用调用虚函数
void doSpeak(Animal &animal) // Animal &animal = cat or dog; 
{
    animal.speak();
}

void test01()
{
    Cat cat;
    doSpeak(cat);   // 如果不加virtual关键字,则输出:“动物在说话”[地址早绑定]。加了virtual关键字,输出:“猫在说话”[地址晚绑定]

    Dog dog;
    doSpeak(dog);   // 输出:“狗在说话”
}

void test02()
{
    cout << "size of Animal: " << sizeof(Animal) << endl;
    cout << "size of Cat: " << sizeof(Cat) << endl; 
    cout << "size of Dog: " << sizeof(Dog) << endl;
}
// 使用指针调用虚函数
void test03()
{
    Animal* a;
    a = new Cat();
    a->speak();     // 输出:“猫在说话”

    a = new Dog();
    a->speak();     // 输出:“狗在说话”
}

int main()
{
    // test01();
    // test02();
    test03();
}

在这里插入图片描述
以test03为例:
Animal类中的speak()函数被声明为虚函数,派生类中的speak()函数也被声明为虚函数,并且它们的函数签名相同。当调用a->speak()时,编译器会根据a指向的对象的实际类型,调用对应的speak()函数。这种技术称为动态绑定(dynamic binding)。这是C++中实现多态性的基础。

多态案例 - 计算器类

案例描述:分别用普通技术和多态技术,设计实现两个操作数进行运算的计算器类

普通方法实现
#include <iostream>
#include <string>
using namespace std;

// 普通实现
class Calculator
{
private:
    /* data */
public:
    int m_Num1, m_Num2;

    int getResult(string oper)
    {
        if (oper == "+")
        {
            return m_Num1 + m_Num2;
        }
        else if (oper == "-")
        {
            return m_Num1 - m_Num2;     //怎么确定谁是减数,谁是被减数
        }   
        else if (oper == "*")
        {
            return m_Num1 * m_Num2;
        }
        else if (oper == "/")
        {
            return m_Num1 / m_Num2;     //怎么确定谁是除数,谁是被除数
        }
        else
        {
            return 0;
        }  
    }   
};
void test01()
{
    Calculator cal;
    cal.m_Num1 = 10;
    cal.m_Num2 = 20;
    cout << "两数相加:" << cal.getResult("+") << endl;
    cout << "两数相减:" << cal.getResult("-") << endl;
}
int main()
{
    test01();
}
多态方法实现
#include <iostream>
#include <string>
using namespace std;

class AbstractCalculator
{
public:
    int m_Num1, m_Num2;

    virtual int getResult()
    {
        return 0;
    }
};
class AddCalculator:public AbstractCalculator
{
public:
    int getResult()
    {
        return m_Num1 + m_Num2;
    }
};
class SubtractCalculator:public AbstractCalculator
{
public:
    int getResult()
    {
        return m_Num1 - m_Num2;
    }
};
class MultiplyCalculator:public AbstractCalculator
{
public:
    int getResult()
    {
        return m_Num1 * m_Num2;
    }
};

class DivideCalculator:public AbstractCalculator
{
public:
    int getResult()
    {
        return m_Num1 / m_Num2;
    }
};
void test02()
{
    AbstractCalculator* ac;
    ac = new AddCalculator();
    ac->m_Num1 = 20;
    ac->m_Num2 = 10;
    cout << ac->m_Num1 << " + " << ac->m_Num2 << " = " << ac->getResult() << endl;
    // delete ac;

    ac = new SubtractCalculator();
    ac->m_Num1 = 20;
    ac->m_Num2 = 10;
    cout << ac->m_Num1 << " - " << ac->m_Num2 << " = " << ac->getResult() << endl;
    // delete ac;

    ac = new MultiplyCalculator();
    ac->m_Num1 = 20;
    ac->m_Num2 = 10;
    cout << ac->m_Num1 << " * " << ac->m_Num2 << " = " << ac->getResult() << endl;
    // delete ac;

    ac = new DivideCalculator();
    ac->m_Num1 = 20;
    ac->m_Num2 = 10;
    cout << ac->m_Num1 << " / " << ac->m_Num2 << " = " << ac->getResult() << endl;
}
int main()
{
	test02();
}
/* 输出结果:
20 + 10 = 30
20 - 10 = 10
20 * 10 = 200
20 / 10 = 2		*/

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现都是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;

当类中有个纯虚函数,这个类也称为抽象类
抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
#include <iostream>
using namespace std;
// 抽象类
class Base
{
public:
	virtual void func() = 0;
};

class Son:public Base
{
public:
	void func()
	{
		cout << "Son -> func()" << endl;
	}
};
int main()
{
	Son s;
}

多态案例 - 制作饮品

案例描述:
制作饮品的基本步骤1.煮水 2.冲泡 3.倒入杯中 4.加入辅料
使用多态方法实现,使用抽象类作为基类,制作咖啡和茶
咖啡步骤:1.煮水 2.冲泡咖啡 3.倒入杯中 4.加入糖和奶
茶步骤:1.煮水 2.冲泡茶叶 3.倒入杯中 4.加入柠檬

#include <iostream>
#include <string>
using namespace std;

class AbstractDrink
{
public:
    //煮水
    virtual void Boil() = 0;
    //冲泡
    virtual void Brew() = 0;
    //倒入杯中
    virtual void PourInCup() = 0;
    //加入辅料
    virtual void AddMat() = 0;

    void makeDrink()
    {
        Boil();
        Brew();
        PourInCup();
        AddMat();
    }
};

class MakeCoffee:public AbstractDrink
{
public:
    void Boil()
    {
        cout << "1.煮水" << endl;
    }
    void Brew()
    {
        cout << "2.冲泡咖啡" << endl;
    }
    void PourInCup()
    {
        cout << "3.倒入杯中" << endl;
    }
    void AddMat()
    {
        cout << "4.加糖和牛奶" << endl; 
    }
};

class MakeTea:public AbstractDrink
{
public:
    void Boil()
    {
        cout << "1.煮水" << endl;
    }
    void Brew()
    {
        cout << "2.冲泡茶叶" << endl;
    }
    void PourInCup()
    {
        cout << "3.倒入杯中" << endl;
    }
    void AddMat()
    {
        cout << "4.加柠檬" << endl; 
    }
};

void test01()
{
    AbstractDrink *mk;
    mk = new MakeCoffee();
    mk->makeDrink();
    // delete mk;
    cout << "---------------" << endl;
    mk = new MakeTea();
    mk->makeDrink();
}

int main()
{
    test01();
}

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方法:将父类中的析构函数改为虚析构纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构的区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象
  • 纯虚析构需要声明,也需要实现

虚析构语法:virtual ~类名(){}
纯虚析构语法:

  • virtual ~类名()=0; ->声明
  • 类名::~类名(){} -> 实现

如果子类中没有堆区数据,可以不写虚析构或纯虚析构

多态案例 - 电脑组装

案例描述:电脑主要组成部件有CPU、显卡、内存条;
将每个组件封装成抽象类,并提供不同的厂商生产不同的零件,例如:Intel、Lenovo;创建电脑类提供让电脑工作的函数,并调用每个零件工作的接口。测试时组装三台不同的电脑进行工作。

  • 具体的代码实现待补充…

C++文件操作

程序运行时所产生的数据都是临时数据,程序一旦运行结束就会被释放。
通过文件可以将数据持久化
C++中对文件操作需要包含头文件<fstream>
文件类型分两种:

  • 文本文件 :文件以文本的ASCII码形式存储在计算机中
  • 二进制文件 :文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们

操作文件的三大类:

  1. ofstream : 写操作
  2. ifstream : 读操作
  3. fstream : 读写操作

文件打开方式:

打开方式解释
ios::in为读文件而打开文件
ios::out为写文件而打开文件
ios::ate初始位置:文件尾
ios::app追加方式写入
ios::trunc如果文件存在先删除再创建
ios::binary二进制方式(二进制文件一定要指定此方式)

Tips: 文件打开方式可以配合使用,使用“|”操作符。

文本文件 - 写文件

写文件的步骤:

  1. 包含头文件<fstream>
  2. 创建流对象
  3. 指定打开方式和文件路径
  4. 写内容
  5. 关闭文件
#include <iostream>
#include <fstream>	// 1.包含头文件

using namespace std;

void test01()
{
    ofstream ofs;	// 2.创建流对象
    ofs.open("test.txt", ios::out);	 // 3.指定打开方式和文件路径
    ofs<<"姓名:张三" << endl;	// 4.写内容
    ofs<<"性别:男" << endl;		
    ofs.close();			// 5.关闭文件
}

int main()
{
    test01();
}

文本文件 - 读文件

读文件步骤:

  1. 包含头文件<fstream>
  2. 创建流对象
  3. 打开文件并判断文件是否成功打开
  4. 读取数据 (有四种方式)
  5. 关闭文件
#include <iostream>
#include <fstream>	// 1.包含头文件
#include <string>

using namespace std;

void wirteToFile()
{
    ofstream ofs;
    ofs.open("test.txt", ios::out);
    ofs<<"姓名:张三" << endl;
    ofs<<"性别:男" << endl;
    ofs<<"年龄:28" << endl;
    ofs.close();
}

void readDataFromFile()
{
    ifstream ifs;	// 2. 创建流对象
    // 3. 打开文件并判断文件是否成功打开
    ifs.open("test.txt", ios::in);
    if (!ifs.is_open())
    {
        cout << "文件打开失败" << endl;
    }
    /* 读取数据(有四种方式) */
    // 第一种
    char buf[1024] = {0};
    while (ifs >> buf)
    {
    	cout << buf << endl;
    }
    // 第二种
    char buf[1024] = {0};
    while (ifs.getline(buf, sizeof(buf)))
    {
        cout << buf << endl;
    }
    // 第三种
    string buf;
    while (getline(ifs, buf))
    {
        cout << buf << endl;
    }
    // 第四种	“不推荐此种方法”
    char c;
    while ((c = ifs.get()) != EOF)  // EOF -> end of file
    {
        cout << c;
    }
    ifs.close();  
}

int main()
{
    wirteToFile();
    readDataFromFile();
}

二进制文件 - 写文件

二进制方式写文件主要利用流对象调用成员函数write
函数原型:ostream& write(const char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是写的字节数

二进制文件 - 读文件

二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer, int len);
参数解释:字符指针buffer指向内存中一段存储空间,len是读的字节数

#include <iostream>
#include <fstream>	// 1.包含头文件

using namespace std;

class Person
{
public:
    char m_Name[64];
    int m_Age;
};

void binWrite()
{
    ofstream ofs;   // 2.创建流对象
    ofs.open("person.txt", ios::out | ios::binary); // 3.打开文件
    Person p={"李四", 28};
    ofs.write((const char *) &p, sizeof(Person));   // 4.写入内容
    ofs.close();    // 5.关闭文件
}

void binRead()
{
    ifstream ifs;   // 2.创建流对象
    ifs.open("person.txt", ios::in | ios::binary);  // 3.打开文件
    if (! ifs.is_open())
    {
        cout << "文件打开失败" << endl;
        return;
    }
    Person pp;
    ifs.read((char *)&pp, sizeof(Person));   // 4.读取内容
    cout << "name: " << pp.m_Name << " age: " << pp.m_Age << endl;
    ifs.close();    // 5.关闭文件
}

int main()
{
    binWrite();
    binRead();
}

练习项目

背景:
在这里插入图片描述
实现代码github地址:职工系统的代码实现

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值