文章目录

1. 类和对象


  • 面向过程与面向对象的程序设计的区别
    • 面向过程程序=算法+数据结构,在面向过程编程中,算法和数据结构是相互独立的,一组或多组算法处理多组数据,或一组数据被一组或多组算法处理;
    • 面向对象对象=算法+数据结构,程序=(对象+对象+…+对象)+消息,在面向对象编程中,一组算法和一组数据是封装成对象的,在同一个对象中,一个算法只处理一组数据,可以把对象想象成一个小程序,而总体程序由多个小程序组成,小程序之间通过信息交流;

面向对象是C++的一种数据类型,类是这种数据类型的格式规范,它规范了成员数据和成员函数的格式,对象是类的实体;

对比维度 面向过程(C 语言为主) 面向对象(C++/Java/C#)
设计思想 关注步骤/流程:先做什么,后做什么 关注对象/事物:谁来完成这些事
代码单位 函数为基本单位 类/对象为基本单位
数据与行为 数据和函数分离,互不隶属 属性+行为封装一体,属于对象
耦合性 耦合高,修改一处易影响全局 低耦合,模块独立,互不干扰
安全性 全局变量多,易被误改,不安全 权限控制(private/protected),安全可控
复用性 复用函数,复制粘贴多 复用类、继承,扩展方便
维护性 项目变大后难维护、乱 大型项目结构清晰,易维护
典型写法 motor_init(); motor_run(); motor.init(); motor.run();
适合场景 小型项目、简单逻辑、裸机驱动 中大型项目、多设备、复杂业务
  • 对象(object):客观世界中任何一个事物都可以看成一个对象;

    • 对象的两个要素:属性(attribute)和行为(behavior),对象是由一些属性和一些行为组成的,属性即对象固有的特性(数据),行为即对象根据外部消息而改变的功能(函数);
  • 消息(message):向对象发送一个消息,从外部控制对象的行为,也是对象之间的信息交流介质;


  • 对象实例化的方式:
实例化方式 代码示例 内存区域 生命周期管理 访问方式 调用构造
栈中直接创建 Person p;
Person p(10);
栈内存 自动分配,自动释放 . 点操作符 无参/有参构造
堆中动态创建 Person *p = new Person();
Person *p = new Person(10);
堆内存 手动 new 创建,必须手动 delete 释放 -> 箭头操作符 无参/有参构造
拷贝方式创建 Person p2(p1);
Person p2 = p1;
栈/堆 同栈/堆对象规则 .-> 拷贝构造函数

  • 面向对象编程的4个主要特点:抽象、封装、继承、多态性;

    1. 抽象(abstraction)、类、对象:抽象即表示同一类事物的共同特征,如人是抽象的,张三是具体的人,国家是抽象的,中国是具体的国家; 类(class) 即具体对象的抽象;对象是类的具体实例(instance),如张三是人类的具体实例,中国是国家类的具体实例;

    类是抽象的,不占用内存,对象是具体的,占用内存空间;

    1. 封装(Encapsulation):一是将属性和行为封装到一个对象中,形成一个整体,各个对象之间相互独立,互不影响;二是将对象的某些部分使用访问权限(public/private/protected)保护,对外只留下少量接口用于对外交流;定义:Class 类名{访问权限:属性/行为}

    2. 继承(Inheritance):利用一个已有的类创建另一个类,重用已有类的一部分属性和行为,节省编程工作量,形成层次关系;”马“是一个类,现在定义另一个类叫”白马“,只需从“马”这个类中添加白色的属性,前者叫父类/基类,后者叫子类/派生类;子类还可以继续继承,如继续定义中国白马;

    3. 多态性(polymorphism):由继承而产生的不同的子类,对同一消息作出不同的功能;实现多态性的函数必须是父类的virtual 虚函数,子类才能重写;

//多态性:

// 父类
class Device {
public:
    // 虚函数
    virtual void run() {
        cout << "设备运行" << endl;
    }
};

// 子类1:电机
class Motor : public Device {
public:
    // 重写父类虚函数
    void run() override {
        cout << "电机转动" << endl;
    }
};

// 子类2:LED
class LED : public Device {
public:
    void run() override {
        cout << "LED闪烁" << endl;
    }
};

int main() {
    // 父类指针指向不同子类对象
    Device* dev;

    dev = new Motor;
    dev->run();   // 输出:电机转动

    dev = new LED;
    dev->run();   // 输出:LED闪烁
}

1.1 封装(访问权限)

  • 访问权限:通过三个关键字控制外部能不能访问其类内的属性和行为:
    • public:公有,外部可以访问
    • private(默认):私有,只有类内部可以访问(包括实例化的对象也不能访问),其子类不能访问;
    • protected:保护,只有类内部可以访问(包括实例化的对象也不能访问),其子类可以访问,方便继承;
// 外部不能直接改speed,只能通过调用接口函数setSpeed 实现,保证数据安全;
class Motor {
private:
    int speed;  // 内部真实速度,外部不能直接改

public:
    // 对外提供安全接口
    void setSpeed(int s) {
        if (s >= 0 && s <= 1000) {
            speed = s;
        }
    }
};

1.2 对象的初始化和清理

  • 构造函数:对象创建时自动调用,用于初始化;
  • 析构函数:对象销毁时自动调用,用于释放资源;

1.2.1 构造函数

构造函数(Constructor)是C++提供的用于处理对象成员数据初始化的一个类中的特殊函数,构造函数不需要用户调用,也不能被用户调用,它在建立对象之初自动且只执行一次,构造函数的名字必须与类名相同,没有返回值(连void 都不用写);

  • 默认构造函数:这种不带任何输入参数的构造函数,称为默认构造函数,每个类中,只允许出现一个默认构造函数;
  • 声明类时若不手动定义构造函数,系统会在类声明时自动生成一个无参构造函数和一个拷贝构造函数;但若手动写了任意一个构造函数,编译器不再生成无参构造函数,但仍会自动生成拷贝构造;如下表格:
用户是否手动写了构造 编译器行为
一个构造函数都没写 自动生成:
• 默认无参构造函数
• 默认拷贝构造函数
写了任意构造(有参 / 拷贝) 不再生成无参构造
仍会生成默认拷贝构造(浅拷贝)
用户写了拷贝构造 使用用户写的,编译器不再生成默认拷贝构造
class Time
{
	private:
		//定义类成员数据
	    int hour;
	    int minute;
	    int sec;
	public:
	    Time() //构造函数
	    {
	        hour=12;
	        minute=12;
	        sec=12;
	    }
	    void set_time();
	    void show_time();
};
void Time::set_time
{...}
void Time::show_time
{...}

int main(void)
{
	//建立对象,同时自动调用构造函数t_1.Time(),所以该对象在被建立初,其成员数据就已经被初始化赋值过
    Time t_1; 
   	t_1.hour=18;
   	Time t_2=t_1; //建立对象t_2,并用对象t_1初始化t_2,此时t_2的数据完全复制对象t_1的数据而不是执行类中的构造函数
    return 0;
}

1.2.1.1 构造函数的分类与调用方式
构造函数的分类 无参构造 有参构造(常用) 拷贝构造
构造函数的调用方式 括号法(常用) 显式法 隐式转换法
  • 构造函数的分类
  1. 无参构造:默认构造;
  2. 有参构造(常用):又分一般参数、带默认值参数、带参数初始化列表的有参三种,详见下面;
  3. 拷贝构造:用一个已经存在的对象,去初始化另一个同类型的新对象;详见下面;

2.1 带输入参数的构造函数:为了给不同的对象赋予不同的初始值,构造函数允许带输入参数(如果输入参数是同类的对象,那么又叫拷贝构造函数);因为构造函数不允许被调用,它只能在建立对象之初作为实参传给对象;因为构造函数允许有输入参数,因此也支持重载;

class Person {
	private:
	    int age;
	    string name;
	
	public:
	    // 1. 无参构造函数
	    Person() {
	        age = 0;
	        name = "无名氏";
	        cout << "无参构造" << endl;
	    }
	
	    // 2. 带一个参数的构造
	    Person(int a) {
	        age = a;
	        name = "无名氏";
	        cout << "带int参数构造" << endl;
	    }
	
	    // 3. 带两个参数的构造
	    Person(int a, string n) {
	        age = a;
	        name = n;
	        cout << "两个参数构造" << endl;
	    }

		// 4. 拷贝构造函数
		Person(const Person &p){
			age = p.age;
			name = p.name ;
			cout << "拷贝构造函数" << endl;
		}
};

int main() {
    Person p1;          // 匹配无参
    Person p2(18);      // 匹配 int
    Person p3(20, "张三"); // 匹配 int + string
    Person p2 = p1;  // 调用拷贝构造
}

2.2 带参数初始化列表的构造函数:在构造函数上更进一步,就有了更简便高效的参数初始化列表;在成员变量为const、成员变量为引用这两种情况时,必须使用该方法;

class Student
{
	private:
	    int num;
	    char name[20];
	    
	public:
	    Student(int n,name[]):num(n),name(name) //直接将传入的实参传给num、name
	    {
	        ...
	    }
// 与下面构造函数写法功能一致:	    
//		Student(int n, name[]) //直接将传入的实参传给num、name
//			{
//				num = n;
//				name[] = "name";
//			}


};

Student stud_1(10101,"zhang_san") //定义对象并初始化
class A {
    const int id;  // const 必须初始化
public:
    // 错误写法
    // A(int i) { id = i; }

    // 正确写法
    A(int i) : id(i) {}
};
class B {
    int &ref; // 成员变量为引用
public:
    B(int &r) : ref(r) {}
};

2.3 带默认值的构造函数:构造函数中成员数据的值既可通过实参传递,也可以指定为某些默认值,即如果用户不给出实参值,编译系统就使用默认值,这样可以减少一定的输入量;

class Box
{
public:
    Box(int h=10,int w=10,int len=10); //声明带默认值的构造函数
    Box(); //默认构造函数
    Box(int h,int w,int len) //构造函数重载
private:
    int height;
    int width;
    int length;
};
Box::Box(int h,int w,int len) //类外定义构造函数
{
    height=h;
    width=w;
    length=len;
}
int main(void)
{
    Box box_1; //实例化对象,使用默认的构造函数,全部成员数据使用默认值
    Box box_2(15);//实例化对象,使用默认的构造函数,第一个成员数据height使用给定值,其他使用默认值
    Box box_3(15,30);//实例化对象,使用默认的构造函数,第一、二个成员数据使用给定值,第三个成员length数据使用默认值
    Box box_4(15,30,20);//实例化对象,使用默认的构造函数,全部成员数据使用给定值
    // 显然对象box_4的定义会引起歧义,究竟使用重载的构造函数还是带默认值的构造函数?
}

一旦类中定义了带默认值的构造函数,就不应在定义重载构造函数,会出现歧义性;

  1. 拷贝构造调用的时机一般有三种
    • 使用一个已创建的对象来初始化一个新对象;
    • 函数的参数是值传递的对象;
    • 函数以值方式返回局部对象;
- 使用一个已创建的对象来初始化一个新对象:
Person p1;        // 先有 p1
Person p2 = p1;   // 调用拷贝构造
Person p3(p1);    // 调用拷贝构造,功能同上

-  函数的参数是值传递的对象:
void func(Person p1) { ... }

int main() {
    Person p;
    func(p);  // 实参对象p 拷贝给函数形参p1,调用拷贝构造,在函数func 内对形参p1 进行修改,并不会影响被拷贝的对象p,他俩在不同的内存空间
}

- 函数以值方式返回局部对象:
Person func() {
    Person p;
    return p;  // 返回时会用p 拷贝一个临时对象
}
  • 构造函数的调用方式
  1. 括号法(常用);
  2. 显式法;
  3. 隐式转换法;
1. 括号法:
Person p;
Person p(18);

2. 显式法:
Person p1 = Person(18);
Person p2 = Person(p1); // 拷贝构造的显式法调用

3. 隐式转换法:
Person p1 = 18; // 等同于上面Person p1 = person(18);
Person p2 = p1; // 等同于上面Person p2 = Person(p1);

1.2.1.2 浅拷贝与深拷贝
  • 浅拷贝(默认):简单赋值,只拷贝值,多个指针指向同一块内存;
  • 深拷贝(推荐):重新开辟内存,拷贝数据,不同指针各自指向各自的内存;
对比项 浅拷贝 (Shallow Copy) 深拷贝 (Deep Copy)
实现方式 编译器默认拷贝构造 手动编写拷贝构造
处理指针 只拷贝地址 在堆区重新开辟内存,再拷贝内容
内存关系 多个对象共用同一块堆内存 每个对象拥有独立内存
优点 简单、速度快 安全、互不干扰
缺点 会造成重复释放内存、数据互相污染 稍占内存、速度略慢
适用场景 类中无指针、无动态内存 类中有 new/ 指针,必须使用深拷贝
class Person {
public:
    int* age; // 创建一个指针,用于深拷贝时指向变量的动态内存

    Person(int a) {
        age = new int(a); // 堆上分配,类中有动态内存创建!
    }

   // 1浅拷贝:不写拷贝构造 → 编译器自动浅拷贝
  
	// 2深拷贝:用户自己写拷贝构造 → 深拷贝
	Person(const Person &p) {
	        // 1. 新开内存;2. 拷贝数据
	        age = new int(*p.age);
	    }

    ~Person() {
    		if (age != nullptr)
			{
	        delete age; 
	        // 如果是浅拷贝,那么会发生重复delete age(原对象和拷贝对象都执行一次 delete age,但他俩都指向同一个age 内存),系统崩溃
	        // 如果是深拷贝,那么这里是正常的堆区内存的手动释放
	        age = nullptr; // 被释放的指针指向空,防止变成野指针
    		}
    }
};

int main() {
    Person p1(18);
    Person p2 = p1; 
    // 1浅拷贝,p1和p2的age 指针同地址
    // 2深拷贝,p1和p2的age 指针不同地址
}

1.2.2 析构函数

析构函数(destructor)与构造函数对应,也是一种特殊的成员函数,其作用跟构造函数相反;语法也是在构造函数的函数名前加取反运算符~即成了析构函数;当对象的生命周期结束时,会自动执行析构函数,它的作用是在系统撤销对象占用的内存空间之前完成一些清理工作,它没有任何输入参数、返回值,也不能重载;

class Student
{
	private:
	    int num;
	    char name[10];
	    char sex;
	    
	public:
	    Student(int,string,char); //声明构造函数
	    ~Student(); //声明析构函数
	    void display(); //声明成员函数
};
Student::Student(int n,string name,char s) //定义构造函数
{
    num=n;
    name=name;
    sex=s;
}
Student::~Student() //定义析构函数
{
    cout<<"Destructor called."
        <<"Program finished."
        <<'\n';
}

int main(void)
{
    Student stu_1(10010,"Zhang_san","m")
    stud_1.display();
    return 0;
}

1.2.3 类对象作为类成员

在一个类中,把另一个类的对象当作自己的成员变量;

class A {
    // ...
};

class B {
    A a;  // 对象成员:A 类的对象a,是B 的类成员
};
  • 一个类的对象作为另一个类的成员情况下,构造函数与析构函数的调用顺序:跟栈的方式一样,先进者后出,后进者先出;以上面代码为例,先调用类A 的构造函数,再调用类B 的构造函数;析构时,先调用类B
    在这里插入图片描述

  • 如果成员对象类内没有无参构造,必须在当前类的构造函数初始化列表显式调用它的构造:

// 类 A 只有带参构造,没有无参构造
class A {
public:
    A(int x) {}
};

class B {
    A a;  // B 包含对象成员 a
public:
    // 必须在初始化列表里手动调用 A 的有参构造
    B() : a(10) {
    }
};

1.2.4 静态成员static

静态成员就是在成员变量或成员函数前加上关键字static;它属于类,但不属于某一个具体对象;所有对象共享同一份数据,占同一份内存;不依赖具体对象,可以通过类名直接访问;

静态成员也有访问权限;

把类看作建学校的蓝图,成员数据看作学校内建筑,现以该蓝图建立一个大学城,为节约土地资源

  • static定义的建筑(如体育场、美术馆等)为该大学城内所有学校共有资源,其在大学城内只占用一份土地,每个学校都可以使用它,
  • 不用static定义的建筑为各学校私有的资源(如宿舍、食堂、图书馆等每个学校内都有的建筑),
  • 可见,每个学校都可将共用资源认为是自己拥有的资源,当这些共有资源发生变化时,将会影响到该大学城内的所有学校;
  • 静态成员变量:静态成员必须类内声明,类外初始化;
class Box
{
private:
	static int h; // 定义h为静态成员数据
	int w;
	int l;

public:
	int volume();
 } 

int Box::h=10; // 静态成员变量必须在类外进行初始化

Box::h=20; // ✅初始化后,即可通过类名进行访问/修改

Box p;
p.h = 10; // ✅通过对象访问,效果同上
// 此时,对象p 修改了h 为10,那么另一个同类对象访问的h 也变为10(因为都是访问同一个内存)
  • 静态成员函数
    同理,类成员函数也可被static定义为静态,其功能同静态成员数据;因为静态成员函数不属于某个对象,所以它没有this指针;静态成员函数只能访问静态数据成员,而不能访问普通成员变量、普通成员函数(无法区分是哪个具体对象的);
class Box
{
private:
	static int h; 
	int w;
	int l;

public:
	static int volume(){   // 定义静态成员函数 
	// 函数体
	} 
 }

Box::volume(); // ✅可以直接以类调用静态成员函数

Box a(1,2,3); // 建立对象
a.volume();// ✅通过对象访问

1.3 C++ 对象模型与this指针

  1. 空类的对象占用1个字节空间:C++ 编译器会给每个空对象也分配1个字节空间,目的是让每个空对象拥有唯一的内存地址,从而区分不同的空对象;
// 空类
class Person{};

int main() {
    Person obj1, obj2;

    // 1. 打印对象大小:输出 1
    cout << "obj1 size: " << sizeof(obj1) << endl;
    cout << "obj2 size: " << sizeof(obj2) << endl;

    // 2. 打印对象地址:两个地址完全不同
    cout << "obj1 address: " << &obj1 << endl;
    cout << "obj2 address: " << &obj2 << endl;

    return 0;
}
  1. 非空类对象的内存占用:接上例程,若非空类的内部定义了非静态成员变量,那么其创建的对象的大小根据非静态成员变量而定;C++ 编译器不再为空类的对象分配1 字节,因为已经有成员变量占用内存,对象自然拥有唯一内存地址;
// 空类
class Person{
	int m_a; // 非静态成员变量,int 类型占4 字节
};

int main() {
    Person obj1, obj2;

    // 1. 打印对象大小:输出 4
    cout << "obj1 size: " << sizeof(obj1) << endl;
    cout << "obj2 size: " << sizeof(obj2) << endl;

    // 2. 打印对象地址:两个地址完全不同
    cout << "obj1 address: " << &obj1 << endl;
    cout << "obj2 address: " << &obj2 << endl;

    return 0;
}

1.3.1 成员变量与成员函数的内存存储

只有成员变量占用对象内存,成员函数不占用对象内存,所有对象共享一份;sizeof(对象) 只计算非静态成员变量;

内容 存储位置 是否占用对象内存 是否每个对象独立
非静态成员变量 对象内存(栈/堆) ✅ 占用 ✅ 独立
静态成员变量 全局数据区 ❌ 不占用 ❌ 共享
非静态成员函数 代码段 ❌ 不占用 ❌ 共享
静态成员函数 代码段 ❌ 不占用 ❌ 共享
class Person {
    int age;        // 成员变量
    string name;    // 成员变量
    // 每个 Person 对象都会单独存一份 age 和 name;

	  void show() {}  // 成员函数,无论创建1 个还是1000 个对象,show() 永远只有一份
};
  • 每一个非静态成员函数被同类的不同对象所共享,它通过this指针区分哪个对象正在调用自己;详见“2.3.2 this指针”的内容;

1.3.2 this指针

详见“2.3.2 this指针”的内容;

1.3.3 空指针访问成员函数

空指针(nullptr/NULL)可以调用成员函数,但能不能正常运行,取决于函数内部有没有访问成员变量;

  1. 函数内部没有访问成员变量:
class Person {
public:
    void show() {
        cout << "show 执行了" << endl;
    }
};

int main() {
    Person* p = nullptr; // 创建一个对象指针,由于现在没有现成的对象,它暂时指向空,那么它也是一个空指针
    p->show();           // ✅ 可以正常运行
}
  1. 函数内部有访问成员变量:
class Person {
public:
    void showAge() {
        cout << age << endl; // 访问了成员变量
        // cout << this->age << endl; // 上述语句的显式写法,即this = nullptr(this 指针指向空),相当于nullptr->age
    }
private:
    int age;
};

int main() {
    Person* p = nullptr; 
    p->showAge(); // ❌ 程序崩溃,因为p 为指向空的对象指针,即它不指向某一个具体对象,因而它也不存在成员变量
}
class Person {
public:
    void showAge() {
    	if(this == nullptr){return;}// 增加代码健壮性
        cout << age << endl; // 访问了成员变量
    }
private:
    int age;
};

int main() {
    Person* p = nullptr; 
    p->showAge(); // ✅ 可以正常运行
}

1.3.4 const修饰成员函数(常成员函数)

const修饰的成员函数叫常成员函数(或叫常函数),常函数内不能修改任何普通成员变量;在成员函数后加const,本质上修饰的sthis指针的指向,让其指向的值也不可修改(由2.3.2 节知this指针的指向不可被修改);

class Person {
public:
    void setAge(int a) {
        age = a;    // 普通函数:可以修改
    }

    void show() const { // const 写在函数后面
        // age = 100; ❌ 报错,不能修改成员
        cout << age; // ✅ 可以读
    }

private:
    int age;
};
  • 常对象只能调用常成员函数:用const修饰的对象叫常对象,常对象只能调用常函数;
// 类定义接上例程
int main() {
    const Person p; // 常对象
    // p.setAge(10); ❌ 报错:常对象不能调用非常函数
    p.show();       // ✅ 可以调用常函数
}
  • mutable 成员:被mutable修饰的成员变量,即使在常函数和常对象中,依然能被修改;
class Person {
public:
    void func() const {
        a = 100; // ✅ 允许
    }
private:
    mutable int a; // 特殊声明
};

const Person p; // 常对象
p.a = 200; // ✅ 允许

1.4 友元(friend)

友元是C++ 中一个特殊的语法,它的核心作用是让一个外部函数或其他类,访问另一个类的私有(private)和保护(protected)成员;

把类的比作家,private/protected 比作卧室,一般人不能访问,友元就是朋友friend,只有friend 才能进入卧室;

  • 友元的三种实现
    • 友元函数;
    • 友元类
    • 类成员函数做友元;

注意

  • 友元是单向的:A 是 B 的友元 ≠ B 是 A 的友元;
  • 友元不传递:A 是 B 的友元,B 是 C 的友元 ≠ A 是 C 的友元;
  • 友元不继承:父类的友元,不是子类的友元;
  • 友元函数定义比友元类更安全;
  • 友元能不用就不用,是特殊场景的妥协方案
  • 实际应用中,友元最常用在运算符重载
#include <iostream>
using namespace std;

class Person {
    // 重载 << 必须用友元
    friend ostream& operator<<(ostream& out, Person p);

private:
    int age = 20;
    string name = "李四";
};

// 全局重载 <<
ostream& operator<<(ostream& out, Person p) {
    out << p.name << ":" << p.age;
    return out;
}

int main() {
    Person p;
    cout << p << endl; // 直接输出对象
    return 0;
}

1.4.1 友元函数

在类中对某外部全局函数用friend声明,该函数就成了该类的友元函数

#include <iostream>
using namespace std;

class Person {
    // 直接在类内声明外部函数printAge 是Person 的友元函数
    friend void printAge(Person p);

private:
    int age = 20; // 私有成员
};

// 普通全局函数
void printAge(Person p) {
    cout << "年龄:" << p.age << endl;
}

int main() {
    Person p;
    printAge(p); // 正常运行
    return 0;
}

1.4.2 友元类

让一个类的所有成员函数,都能访问另一个类的私有成员;

#include <iostream>
using namespace std;

class Person {
    // 声明 Teacher 是友元类
    friend class Teacher;

private:
    int age = 20;
    string name = "张三";
};

class Teacher {
public:
    void show(Person p) {
        // 可以直接访问所有私有成员
        cout << p.name << " " << p.age << endl;
    }
};

int main() {
    Person p;
    Teacher t;
    t.show(p);
    return 0;
}

1.4.3 类成员函数做友元

声明其他类中的成员函数为本类的友元函数,让其访问本类的私有成员;

#include <iostream>
using namespace std;

class Person; // 声明类

class Teacher {
public:
    // 这个成员函数要访问 Person 的私有成员变量
    void showPersonAge(Person p);
};

class Person {
    // 声明Teacher 的showPersonAge 是友元函数
    friend void Teacher::showPersonAge(Person p);

private:
    int age = 20;
};

// 类外实现成员函数
void Teacher::showPersonAge(Person p) {
    cout << "学生年龄:" << p.age << endl;
}

int main() {
    Person p;
    Teacher t;
    t.showPersonAge(p);
    return 0;
}

1.5 运算符重载

对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型;运算符本质就是函数,重载就是写一个特殊函数。

比如:

  • +本来只能加数字,你可以让它加两个对象;
  • == 本来比较数值,你可以让它比较两个对象内容;
  • << 本来输出数字,你可以让它直接输出整个对象;

下面内容以“加号运算符重载”的例程为基础,解析运算符重载的原理和应用;


  • 两种重载方式
    • 成员函数重载(推荐):写在类里面(this 指针自动作为左操作数),只需要传一个右操作数;下面“加号运算符重载”例程就是用的该方式;
    • 全局函数重载:写在类外面,通常需要配合friend,需要两个参数:左、右操作数,例程如下:
// 全局函数重载
class Person {
	friend Person operator+(Person& p); // 声明为友元,才能访问 private

public:
    int m_a;
	int m_b;
};

Person  operator+(Person& p1,Person& p2) {
	Person temp;
	temp.m_a = p1.m_a + p2.m_a;
	return temp;	
}

int main() {
    Person p1;
	p1.m_a = 10;

	Person p2;
	p2.m_a = 10;	

	// Person p3 = operator+(p1,p2); // 一般写法
	Person p3 = p1 + p2; // 简易写法
    return 0;
}

  • 运算符重载的重载:运算符重载支持重载,下面以“加号运算符重载”的例程为基础,解析运算符重载的重载的原理和应用;
class Person {
public:
    int m_a;
	int m_b;

public:
	Person operator+(Person& p); // 声明
	Person operator+(int num); // 运算符重载的重载,输入参数类型不同
};

Person Person :: operator+(Person& p) { // 将函数名改为`operator`加`+`
	Person temp;
	temp.m_a = this->m_a + p.m_a;
	return temp;	
}

Person Person :: operator+(int num) { // 将函数名改为`operator`加`+`
	Person temp;
	temp.m_a = this->m_a + num;
	return temp;	
}

int main() {
    Person p1;
	p1.m_a = 10;

	Person p2;
	p2.m_a = 10;	

	// person p3 = p1.personaddperson(p2); // 一般写法1
	// Person p3 = p1.operator+(p2); // 一般写法2
	Person p3 = p1 + p2; // 简化写法,调用Person operator+(Person& p);
	Person p4 = p1 +10; // 调用Person operator+(int num);
    return 0;
}

1.5.1 加号运算符重载+

  1. 现有代码如下:
class Person {
public:
    int m_a;
};

int main() {
    Person p1;
	p1.m_a = 10;

	Person p2;
	p2.m_a = 10;	

    return 0;
}
  1. 现在想要实现一个关于+号的运算符重载,使Person p3 = p1 + p2;语句实现p3.m_a = p1.m_a + p2.m_a的功能,首先以常规方法,在类中创建一个成员函数实现该功能:
class Person {
public:
    int m_a;
	int m_b;

public:
	Person PersonAddPerson(Person& p); // 声明
};

Person Person:: PersonAddPerson(Person& p) { // 创建一个函数,实现`p3.m_a = p1.m_a + p2.m_a`的功能
	Person temp;
	temp.m_a = this->m_a + p.m_a;
	return temp;	
}

int main() {
    Person p1;
	p1.m_a = 10;

	Person p2;
	p2.m_a = 10;	

	Person p3 = p1.PersonAddPerson(p2);
    return 0;
}
  1. 以上面代码为基础,用到C++ 编译器的用法关键字operator实现运算符重载,:
class Person {
public:
    int m_a;
	int m_b;

public:
	Person operator+(Person& p); // 声明
};

Person Person :: operator+(Person& p) { // 将函数名改为`operator`加`+`
	Person temp;
	temp.m_a = this->m_a + p.m_a;
	return temp;	
}

int main() {
    Person p1;
	p1.m_a = 10;

	Person p2;
	p2.m_a = 10;	

	// person p3 = p1.personaddperson(p2); // 一般写法1
	// Person p3 = p1.operator+(p2); // 一般写法2
	Person p3 = p1 + p2; // 简化写法
    return 0;
}

1.5.2 左移运算符重载<<

使用左移运算符重载时必须用友元全局函数,因为cout 是左操作数,不是你的类对象,不能写成成员函数;

class Point {
	friend ostream& operator<<(ostream& out, const Point& p); // 友元
public:
	int x, y;
};

ostream& operator<<(ostream& cout, const Point& p) { // 其中ostream 为cout 的数据类型
	cout << "(" << p.x << "," << p.y << ")";
	return cout;  // 必须返回 ostream& 支持连续输出
}

int main() {
	Point p{1,2};
	cout << p << endl;

	return 0;
}

1.5.3 自增运算符重载++

自增运算符重载需区分前置/后置;自减运算符重载同理;

class Myinteger {
	friend ostream& operator<<(ostream& cout, const Myinteger& myint);

public:
	Myinteger()
	{
		m_num = 0;
	}

	// 重置++运算符(前置)
	Myinteger& operator++() {
		m_num++; // 先自增,再返回当前对象的引用
		return *this;  // 返回当前对象的引用
	}

	// 重置++运算符(后置)
	Myinteger& operator++(int) {// 这里的int参数(占位参数)只是为了区分前置和后置,并不使用
		Myinteger temp = *this;// 先保存当前对象的状态
		m_num++; // 再自增
		return *this;  // 最后将当前对象的状态做返回
	}
private:
	int m_num;
};

ostream& operator<<(ostream& cout, const Myinteger& myint) { // 其中ostream 为cout 的数据类型
	cout << myint.m_num;
	return cout;  // 必须返回 ostream& 支持连续输出
}

int main() {
	Myinteger myint;
	cout << ++myint << endl;  // 调用前置++运算符,先自增后输出
	cout << myint++ << endl; // 调用后置++运算符,先输出当前值后自增
	return 0;
}

1.5.4 赋值运算符重载=

赋值运算符= 只能重载为成员函数,不能写成全局/友元;

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

  1. 默认构造函数(无参,函数体为空);
  2. 默认析构函数(无参,函数体为空);
  3. 默认拷贝构造函数,对属性进行值拷贝;
  4. 赋值运算符 operator=,对属性进行值拷贝(浅拷贝);如果类中有属性指向堆区(指针、new操作),做赋值操作时必须使用深拷贝
  • 深拷贝(推荐):
class Person {
public:
	Person(int age)
	{
		m_age = new int(age);
	}

	~Person()
	{
		if (m_age != NULL)
		{
			delete m_age;
			m_age = NULL;
		}
	}

	Person& operator=(Person& p)
	{
		if (this == &p) // 自赋值检查
		{
			return *this;
		}
		if (m_age != NULL) // 释放原有内存
		{
			delete m_age;
			m_age = NULL;
		}
		m_age = new int(*p.m_age); // 深拷贝,重新申请空间
		return *this;
	}

public:
	int* m_age; // 指针成员变量
};


int main() {
	Person p1(18);
	Person p2(20);
	Person p3(30);

	p3 = p2 = p1;
	
	return 0;
}
  • 浅拷贝
class Person {
public:
    Person& operator=(Person& other) {
        // 1. 防止自赋值
        if (this == &other)
            return *this;

        // 2. 赋值成员
        m_age = other.m_age;

        // 3. 返回自身引用,支持连续赋值 a = b = c
        return *this;
    }

public:
    int m_age;
};


int main() {
	Person p1, p2,p3;
	p1.m_age = 18;
	p2.m_age = 20;
    p3 = p2 = p1; // 调用 operator=,只是简单的值拷贝(浅拷贝)

	return 0;
}

1.5.5 关系运算符重载

关系运算符一律返回bool

  • 支持重载的关系运算符如下:
    在这里插入图片描述
class Person {
public:
	Person(string name, int age) {
		m_name = name;
		m_age = age;
	}

	// 重载 == 运算符
	bool operator==(Person& p) {
		if (this->m_name == p.m_name && this->m_age == p.m_age) {
			return true;
		}
		else {
			return false;
		}
	}

public:
	string m_name;
	int m_age;
};

int main() {
	Person p1("Tom", 18);
	Person p2("Tom", 28);

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

	return 0;
}

1.5.6 函数调用运算符重载()

函数调用运算符即运算符();由于重载后使用的方式非常像函数的调用,故又叫仿函数,在嵌入式和 STL 里都非常常用;

仿函数的写法非常灵活,没有固定形式;可以带任意参数、任意返回值;

class Myptr {
public:
	// 重载函数调用运算符
	void operator()(string text) {
		cout << text << endl;
	}
};

class MyAdd {
public:
	int operator()(int v1, int v2) {
		return v1 + v2;
	}
};

int main() {
	Myptr myPtr;
	MyAdd myAdd;

	myPtr("Hello World!"); // 通过重载函数调用运算符,myPtr对象可以像函数一样被调用

	// 1. 通过重载函数调用运算符,myAdd对象可以像函数一样被调用
	int ret = myAdd(10, 10); 
	cout << "10 + 10 = " << ret << endl;

	// 2. 匿名函数调用
	cout << "MyAdd()(10,10) = "<< MyAdd()(10, 10) << endl; // 效果同上1

	system("pause");
	return 0;
}
  • 匿名函数对象:在使用处直接创建一个“临时对象”,直接调用它,不赋值给任何变量;
class Comparer {
public: 
    bool operator()(int a, int b) const { // 函数调用运算符重载
        return a < b;
    }
};

int main() {
    int arr[] = {5, 2, 9, 1};

    sort(arr, arr + 4, Comparer()); // Comparer() 就是匿名函数对象:建一个临时对象,没有名字,直接传给 sort 当比较函数

    // 打印看看结果
    for (int i = 0; i < 4; i++) {
        cout << arr[i] << " ";
    }
    return 0;
}

1.6 继承

  • 继承:即在一个已有的类的基础上建立一个新的类,前者叫基类(base class)/父类(father class),后者叫派生类(derived class)/子类(son class),子类从父类中获得特性,又可为自身新增另外的属性和方法;继承的最大作用,是提高代码复用性;

如:男人类从人类中继承人的所有特性又增加了雄性的特性,向下还可以有中国男人、美国男人等新类;

  • 单继承:single inheritance,如下图 11.3,子类继承与单一的父类;
  • 多重继承:multiple inheritance,如下图 11.4,子类继承于多个父类;

如:计算机本科继承于计算机专业和本科学历;

在这里插入图片描述

1.6.1 基本语法

最基本的继承语法:

// 父类
class Base {
public:
	void show() {cout << "Base\n"; }
};

// 子类
class Derived : public Base {
	// 自动拥有 Base 的成员
};

int main() {
	Derived d;
	d.show(); // 直接调用父类的成员函数

	system("pause");
	return 0;
}

1.6.2 继承方式

子类的继承方式决定了父类成员到了子类里,会变成什么访问权限;

  • 公有继承public(常用):接口不变,对外公开;
  • 保护继承 protected:接口变家族内部所有;
  • 私有继承private:接口全变自己私有的;
继承方式 父类的public 父类的protected 父类的private
public继承 变public protected 父类的private 始终是父类的私有,对外和子类都不可见
protected继承 protected protected 不可见
private继承 private private 不可见

在这里插入图片描述

  • 父类成员访问权限
    - 父类 public:子类 + 外部都能访问
    - 父类 protected:子类能访问,外部不能
    - 父类 private:谁都不能直接访问(子类也不行)

1.6.3 继承中的对象模型

即当子类继承父类时的内存存储;子类对象大小 = 父类大小 + 子类自己新增的部分大小;父类的 private 成员也会被继承,只是不能访问;

【Son 对象内存模型】

+----------------------+
| Father::int a        |
| Father::int b        |
| Father::int c        |   ← 父类部分(全部继承)
+----------------------+
| Son::int d           |   ← 子类新增
+----------------------+

cout << sizeof(Father) << endl; // 12(a b c 各4字节)
cout << sizeof(Son) << endl;    // 16(12 + d 4字节)

1.6.4 继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数;

  • 函数调用顺序:先构造父类 → 再构造子类;先析构子类 → 再析构父类;

1.6.5 同名成员处理

子类和父类有同名成员时,默认优先用子类;要访问父类,必须加作用域父类名::

  • 同名成员变量
class Father {
public:
	int m_A = 10;
};

class Son : public Father {
public:
	int m_A = 20; // 和父类成员变量同名
};

int main() {
	Son s;

	cout << s.m_A << endl;     // 20,默认用子类
	cout << s.Father::m_A << endl; // 10,指定父类

	system("pause");
	return 0;
}
  • 同名成员函数:若子类中出现于父类同名的成员函数,那么子类的同名成员函数会隐藏父类所有同名函数(包括重载版本);
class Father {
public:
	void func() { cout << "父类func" << endl; }
	void func(int age)	{cout << "父类func - age"<< endl;}
};

class Son : public Father {
public:
	void func() { cout << "子类func" << endl; }
};

int main() {
	Son s;

	s.func();        // 子类func
	s.func(50);      // ❌报错,子类func 会隐藏父类所有同名func
	s.Father::func();// 父类func
	s.Father::func(50);// ✅父类func

	system("pause");
	return 0;
}
  • 静态同名成员:与上述调用方法一样;
class Father {
public:
	static int m_A;
};
int Father::m_A = 10;

class Son : public Father {
public:
	static int m_A;
};
int Son::m_A = 20;

int main() {
	// 通过对象
	Son s;
	s.m_A;        // 子类
	s.Father::m_A;// 父类

	// 通过类名
	Son::m_A;         // 子类
	Father::m_A;      // 父类
	Son::Father::m_A; // 父类(常用)

	system("pause");
	return 0;
}

1.6.6 多继承语法

实际开发中不建议使用多继承

同名成员必须用作用域父类名:: 区分;

// 基本语法
class Base1 {
public:
	int a;
};

class Base2 {
public:
	int b;
};

// 多继承
class Son : public Base1, public Base2 {
public:
	int c;
};

int main() {
	Son s;
	s.a = 10;
	s.b = 20;
	s.c = 30;

	system("pause");
	return 0;
}
  • 构造函数写法:按继承顺序调用父类构造
class Son : public Base1, public Base2 {
public:
    Son() : Base1(), Base2() {
    }
};

1.6.7 菱形继承

实际开发中不建议使用菱形继承

菱形继承即四个类形成一个菱形结构;

问题如下图,结果导致Animal 里的成员变量,在 Alpaca 里存了两份;

        Animal                顶层:公共基类
       /       \
      /         \
     Sheep      Camel        中间:两个类同时继承它
      \         /
       \       /
       Alpaca                底层:一个类同时继承上面两个
class Animal {
public:
	int m_age;
};

class Sheep : public Animal {};
class Camel : public Animal {};

class Alpaca : public Sheep, public Camel {};

int main() {
	Alpaca a;

	a.m_age = 10; // ❌报错!二义性
	// 编译器不知道要哪个 m_age

	a.Sheep::m_age = 10; 
	a.Camel::m_age = 10;// 可用,但数据有两份,资源浪费

	system("pause");
	return 0;
}

---

内存模型:两份 m_age,冗余 + 二义性
class Alpaca    size(8):
        +---
 0      | +--- (base class Sheep)
 0      | | +--- (base class Animal)
 0      | | | m_age
        | | +---
        | +---
 4      | +--- (base class Camel)
 4      | | +--- (base class Animal)
 4      | | | m_age
        | | +---
        | +---
        +---
  • 虚继承virtual:上述问题的解决方法是使用虚继承,在中间两层继承时加上 virtual
// 顶层基类
class Animal { ... };

// 中间层:虚继承,使得中间层的类变为虚基类
class Sheep : virtual public Animal {};
class Camel : virtual public Animal {};

// 底层多继承
class Alpaca : public Sheep, public Camel {}; // 最终子类 只保留一份公共基类成员

---

内存模型:
class Alpaca    size(12):
        +---
 0      | +--- (base class Sheep)
 0      | | {vbptr}   // 虚基类指针(virtual base pointer),该指针指向一个父类(Sheep)的虚基类列表vbtable(virtual base table)
        | +---
 4      | +--- (base class Camel)
 4      | | {vbptr}   // 虚基类指针
        | +---
        +---
        +--- (virtual base Animal)
 8      | m_age
        +---

Alpaca::$vbtable@Sheep@: // 父类(Sheep)的虚基类列表vbtable
 0      | 0
 1      | 8 (Alpacad(Sheep+0)Animal) // 列表中记录一个数据的偏移量,这里是8,即0+8=8,即变量 m_age;

Alpaca::$vbtable@Camel@:
 0      | 0
 1      | 4 (Alpacad(Camel+0)Animal)

1.7 多态

这里只讲动态多态;

多态即父类的指针或引用,调用同一个函数,能表现出不同子类的行为;

多态会让代码量增加,但可让代码组织结构更清晰、可读性更强,对后期扩展和维护性高;

  • 多态分为两类:
    • 静态多态:函数重载、函数模板、运算符重载都属于静态多态,复用函数名;
    • 动态多态:派生类和虚函数实现运行时多态;
    • 两者区别:静态多态在编译期就确定调用哪个函数(早绑定),动态多态在运行时才确定调用哪个函数(晚绑定);
// 静态多态:
void print(int a)    { ... }
void print(double a) { ... }

print(10);    // 编译期就确定调用 int 版本
print(3.14);  // 编译期就确定调用 double 版本


// 动态多态:
class Animal {
public:
    virtual void speak() { ... }
};

class Cat : public Animal {
    virtual void speak() override { ... }
};

Animal* p = new Cat;
p->speak(); // 运行时才确定调用 Cat::speak

1.7.1 基本语法

  • 形成多态的 4 个条件
    • 有继承关系
    • 子类重写父类的虚函数
    • 父类中有virtual 虚函数
    • 用父类指针 / 引用指向子类对象,调用函数
// 父类:有虚函数
class Animal {
public:
	// 虚函数,开启多态;实际应用中更多用纯虚函数
	virtual void speak() {
		cout << "动物叫" << endl;
	}
};

class Cat : public Animal {
public:
	virtual void speak() override {  // virtual 和 override 可选,写上更规范,override 明确表示重写
		cout << "喵喵喵" << endl;
	}
};

class Dog : public Animal {
public:
	virtual void speak() override {
		cout << "汪汪汪" << endl;
	}
};

int main() {
	// 指针方式:
	Animal* animal;

	animal = new Cat; // 父类指针指向子类
	animal->speak();  // 输出:喵喵喵
	delete animal;
	
	animal = new Dog;
	animal->speak();  // 输出:汪汪汪
	delete animal;


	// 引用方式:
	Cat cat;
    Dog dog;

    // 父类引用 绑定 子类对象
    Animal& ref1 = cat;
    ref1.speak();   // 输出:喵喵喵

    Animal& ref2 = dog;
    ref2.speak();   // 输出:汪汪汪


	system("pause");
	return 0;
}

---

内存模型:
class Animal    size(4):
        +---
 0      | {vfptr} // 虚函数表指针(virtual function (table) pointer),该指针指向一个虚函数表(virtual function table)
        +---

Animal::$vftable@: // Animal 类的虚函数表
        | &Animal_meta
        |  0
 0      | &Animal::speak // 表中记录一个函数的函数地址




class Cat       size(4):
        +---
 0      | +--- (base class Animal)
 0      | | {vfptr} // 虚函数表指针,该指针指向一个虚函数表
        | +---
        +---

Cat::$vftable@: // Cat 类的虚函数表
        | &Cat_meta
        |  0
 0      | &Cat::speak // 表中记录一个函数的函数地址,由于使用了虚函数,原先的`&Animal::speak`被`&Cat::speak`覆盖

- 为作对比,此时将Cat 类中的虚函数注释,那么得到以下模型:
class Cat       size(4):
        +---
 0      | +--- (base class Animal)
 0      | | {vfptr} // 虚函数表指针,该指针指向一个虚函数表
        | +---
        +---

Cat::$vftable@: // Cat 类的虚函数表
        | &Cat_meta
        |  0
 0      | &Animal::speak // 表中记录一个函数的函数地址,就是默认的父类的`&Animal::speak`

1.7.2 纯虚函数和抽象类

  • 纯虚函数:只有声明,没有函数体;以 = 0 结尾;必须在子类中重写实现;
virtual void speak() = 0;
  • 抽象类:包含至少一个纯虚函数的类;不能实例化对象;可以定义指针 / 引用(用来实现多态);子类必须重写所有纯虚函数,否则子类也还是抽象类;常用于统一接口规范;
// 例程:
// 抽象类
class Animal {
public:
	virtual void speak() = 0; // 纯虚函数
};

// 子类必须重写纯虚函数
class Cat : public Animal {
public:
	void speak() override {
		cout << "喵喵喵" << endl;
	}
};

class Dog : public Animal {
public:
	void speak() override {
		cout << "汪汪汪" << endl;
	}
};

int main() {
	Animal* p = new Cat;
	p->speak();

	p = new Dog;
	p->speak();

	system("pause");
	return 0;
}

1.7.3 虚析构和纯虚析构

父类指针 delete 子类对象时,为了让子类的析构也能顺利执行,必须把父类析构设为 virtual

类型 写法 作用
普通析构 ~Animal() delete 父类指针时,子类析构不执行
虚析构 virtual ~Animal() 父子析构都执行
纯虚析构 virtual ~Animal()=0; + 类外实现 使类成为抽象类,同时析构安全
  • 虚析构
// 例程
class Animal {
public:
	virtual ~Animal() {  // 虚析构
		cout << "~Animal()" << endl;
	}
};

class Cat : public Animal {
public:
	virtual ~Cat() override {
		cout << "~Cat()" << endl;
	}
};

int main() {
	Animal* p = new Cat;
	delete p;

	system("pause");
	return 0;
}

运行结果:
~Cat()
~Animal()
  • 纯虚析构
class Animal {
public:
	// 纯虚析构声明
	virtual ~Animal() = 0;
};

// 必须在类外实现
Animal::~Animal() {
	cout << "~Animal()" << endl;
}


class Cat : public Animal {
public:
	virtual ~Cat() override {
		cout << "~Cat()" << endl;
	}
};

int main() {
	Animal* p = new Cat;
	delete p;

	system("pause");
	return 0;
}

运行结果:
~Cat()
~Animal()

2. 文件操作

文件操作的目的是让程序能够对磁盘上的文件进行读写,程序运行结束、断电后数据不丢失,实现数据的持久化存储;

  • 文件操作三大类
fstream // 读写操作(常用)
ofstream // 写操作
ifstream // 读操作
  • 文件打开方式:可通过操作符| 组合使用,如ofstream ofs("a.txt", ios::out | ios::app);// 写操作,为写文件而打开文件和追加写入模式
ios::in          // 为读文件而打开文件
ios::out         // 为写文件而打开文件
ios::ate         // 初始位置:文件尾
ios::app         // 追加
ios::trunc       // 清空文件(默认)
ios::binary      // 二进制方式

2.1 文本文件

文本文件即文件以文本的ASCII 码形式存储在计算机中;

2.1.1 写文件

  • 写文件步骤
    • 包含头文件:#include <fstream>
    • 创健流对象:ofstream ofs;
    • 打开文件:ofs.open("文件路径",打开方式);
    • 写数据:ofs<<"写入的数据";
    • 关闭文件:ofs.close();

  • 写入文件(覆盖模式)
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

// 写入文件
void writeFile(const string& filename) {
    // 1. 创建输出文件流,并打开文件
    ofstream ofs(filename);

    // 判断是否打开成功
    if (!ofs.is_open()) {
        cout << "打开失败" << endl;
        return;
    }

    // 2. 像 cout 一样写入
    ofs << "姓名: 张三" << endl;
    ofs << "年龄: 20" << endl;
    ofs << "性别: 男" << endl;

    // 3. 关闭文件
    ofs.close();
}

int main()
{
    writeFile("test.txt");

    return 0;
}
  • 追加写入(不清空原有内容)
// 追加模式
void appendFile(const string& filename) {
    // ios::app 即打开方式为append 追加
    ofstream ofs(filename, ios::app);
    
    if (!ofs.is_open()) {
        cout << "打开失败" << endl;
        return;
    }
    
    ofs << "------------------" << endl;
    ofs << "追加一行内容" << endl;

    ofs.close();
}

int main()
{
    appendFile("test.txt");

    return 0;
}
  • 逐个字符写入
// 按字符写
void writeByChar(const string& filename) {
    ofstream ofs(filename); // 覆盖模式
    
    if (!ofs.is_open()) {
        cout << "打开失败" << endl;
        return;
    }
    
    ofs.put('H');
    ofs.put('i');
    ofs.put('\n');

    ofs.close();
}

int main()
{
    writeByChar("test.txt");

    return 0;
}

2.1.2 读文件

  • 读文件步骤
    • 包含头文件:#include <fstream>
    • 创健流对象:ifstream ifs;
    • 打开文件并判断文件是否打开成功:ifs.open("文件路径",打开方式);
    • 读数据:四种方式读取;
    • 关闭文件:ifs.close();

  • 按行读取(字符串方式)(常用)
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

// 按行读取
void readLine(const string& filename) {
	ifstream ifs(filename); // 创建输入文件流对象,并打开文件

    if (!ifs.is_open()) {
		cout << "文件打开失败" << endl;
        return;
    }
    
    string line;
    // getline(流, 字符串)
    while (getline(ifs, line)) {
        cout << line << endl;
    }

    ifs.close();
}

int main()
{
    readLine("test.txt");

    return 0;
}

  • 按行读取(字符方式):
// 按行读取
void readLinechar(const string& filename) {
    ifstream ifs(filename); // 创建输入文件流对象,并打开文件

    if (!ifs.is_open()) {
        cout << "文件打开失败" << endl;
        return;
    }

    char buf[1024] = {0};
    // ifs.getline(流, 字符)
    while (ifs.getline(buf, sizeof(buf))) {
        cout << buf << endl;
    }

    ifs.close();
}

int main()
{
    readLinechar("test.txt");

    return 0;
}
  • 按单词读取

// 读取文件
void readFile(const string& filename) {
    ifstream ifs(filename);

    if (!ifs.is_open()) {
        cout << "文件不存在" << endl;
        return;
    }

    // 方式1:按空格/换行读取(类似 cin)
    string buf;
    while (ifs >> buf) {    // 读到文件尾自动结束
        cout << buf << endl;
    }

    ifs.close();
}

int main()
{
    readFile("test.txt");

    return 0;
}
  • 一次性读完整个文件到字符串
// 一次性读完整个文件到字符串
void readAll(const string& filename) {
    ifstream ifs(filename);
    string content((istreambuf_iterator<char>(ifs)),
        istreambuf_iterator<char>());

    cout << content << endl;
    ifs.close();
}

int main()
{
    readAll("test.txt");

    return 0;
}
  • 逐个字符读取(不推荐):
void readByChar(const string& filename) {
    ifstream ifs(filename);
    char c;

    while ((c = ifs.get()) != EOF) { // EOF 即End of File
        cout << c;
    }
    ifs.close();
}

int main()
{
    readByChar("test.txt");

    return 0;
}

2.2 二进制文件

二进制文件即文件以二进制的形式存储在计算机中;打开方式必须指定为ios::binary

2.2.1 写文件

二进制文件写操作时,尽量不要操作string型数据,而用char,避免程序崩溃;如果要操作string型数据,见2.2.3节;

  • 写文件步骤
    • 包含头文件:#include <fstream>
    • 创健流对象:ofstream ofs;
    • 打开文件:ofs.open("文件路径",打开方式 | ios::binary);
    • 写数据:ofs.write();
    • 关闭文件:ofs.close();

  • 写入结构体(常用):
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

struct Test { // 声明结构体
    int id;
    float score;
};

void writeStruct(const string& filename, const Test& t) {
    ofstream ofs(filename, ios::binary);

    if (!ofs) {
        cout << "打开失败" << endl;
        return;
    }

    ofs.write((const char*)&t, sizeof(t));

    ofs.close();
}

int main()
{
    Test t = { 1001, 95.5f }; // 定义结构体
    writeStruct("test.bin", t);

    return 0;
}
  • 写入数组
int arr[] = { 1,2,3,4,5 };

void writeArray(const string& filename, int* arr, size_t size) {
    ofstream ofs(filename, ios::binary);


    if (!ofs) {
        cout << "打开失败" << endl;
        return;
    }

    ofs.write((char*)arr, sizeof(int) * size);

    ofs.close();
}

int main()
{
    writeArray("test.bin", arr, sizeof(arr) / sizeof(arr[0]));

    return 0;
}
  • 写入类对象
class Demo {
public:
    int x = 10;
    int y = 20;
};

void writeClass(const string& filename, const Demo& obj) {
    ofstream ofs(filename, ios::binary);

    if (!ofs) {
        cout << "打开失败" << endl;
        return;
    }

    ofs.write((const char*)&obj, sizeof(obj));

    ofs.close();
}

int main()
{
    Demo obj;
    writeClass("test.bin", obj);

    return 0;
}
  • 写入单个变量
int a = 12345;

void writeBasic(const string & filename) {
    ofstream ofs(filename, ios::binary);
    
     if (!ofs) {
        cout << "打开失败" << endl;
        return;
    }
    
    ofs.write((char*)&a, sizeof(a)); // (char*)是将int 数据强转换为字符型

    ofs.close();
}

int main()
{
    writeBasic("test.bin");

    return 0;
}

2.2.2 读文件

  • 读文件步骤
    • 包含头文件:#include <fstream>
    • 创健流对象:ofstream ifs;
    • 打开文件:ifs.open("文件路径",打开方式 | ios::binary);
    • 写数据:ifs.read();
    • 关闭文件:ofs.close();

  • 读取结构体(常用):
#include <iostream>
#include <fstream>
#include <string>
using namespace std;

struct Test { // 声明结构体
    int id;
    float score;
};

void readStruct(const string& filename, Test& t) {
    ifstream ifs(filename, ios::binary);

    if (!ifs) {
        cout << "打开失败" << endl;
        return;
    }

    ifs.read((char*)&t, sizeof(t));

    cout << "id: " << t.id << "  score: " << t.score << endl;
    ifs.close();
}

int main()
{
    Test t; // 定义结构体
    readStruct("test.bin", t);

    return 0;
}
  • 读取数组:
int arr[5];

void readArray(const string& filename, int* arr) {
    ifstream ifs(filename, ios::binary);

    if (!ifs) {
        cout << "打开失败" << endl;
        return;
    }

    ifs.read((char*)arr, sizeof(int) * 5);

    for (int i = 0; i < 5; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
    ifs.close();
}

int main()
{
    readArray("test.bin", arr);

    return 0;
}
  • 读取类对象
class Demo {
public:
    int x = 10;
    int y = 20;
};

void readClass(const string& filename,Demo obj) {
    ifstream ifs(filename, ios::binary);

    if (!ifs) {
    cout << "打开失败" << endl;
    return;
    }

    ifs.read((char*)&obj, sizeof(obj));

    cout << "x=" << obj.x << " y=" << obj.y << endl;
    ifs.close();
}

int main()
{
    Demo obj;
    readClass("test.bin", obj);

    return 0;
}
  • 单个变量
int a;

void readBasic(const string& filename,int a) {
    ifstream ifs(filename, ios::binary);

    if (!ifs) {
    cout << "打开失败" << endl;
    return;
    }

    ifs.read((char*)&a, sizeof(a)); // (char*)是将int 数据强转换为字符型

    cout << "int = " << a << endl;
    ifs.close();
}

int main()
{
    readBasic("test.bin",a);

    return 0;
}
  • 读取指定位置(文件指针跳转)
int a = 12345;

void readSeek(const string& filename, int val) {
    ifstream ifs(filename, ios::binary);

    if (!ifs) {
    cout << "打开失败" << endl;
    return;
    }
 
    // 跳转到第 2 个 int
    ifs.seekg(sizeof(int) * 1);

    ifs.read((char*)&val, sizeof(val));
    cout << "跳转后读取:" << val << endl;

    ifs.close();
}

int main()
{
    readSeek("test.bin",a);

    return 0;
}

2.2.3 读写string型数据

因为 string 内部存的是指针,不是真实字符串;直接读写会把指针写进二进制文件,再读出来就是野指针,程序崩溃;正确的读写操作如下:

string s = "Hello 二进制 string";

// ================== 写入字符串(二进制)==================
void writeStr(const string& filename,string s) {
    ofstream ofs(filename, ios::binary);

    if (!ofs) {
    cout << "打开失败" << endl;
    return;
    }
 
    // 1. 先写入字符串长度
    int len = s.size();
    ofs.write((char*)&len, sizeof(len));

    // 2. 再写入真实字符内容
    ofs.write(s.c_str(), len);

    ofs.close();
}

// ================== 读取字符串(二进制)==================
void readStr(const string& filename) {
    ifstream ifs(filename, ios::binary);

    if (!ifs) {
    cout << "打开失败" << endl;
    return;
    }
 
    // 1. 先读长度
    int len;
    ifs.read((char*)&len, sizeof(len));

    // 2. 准备空间,再读数据
    string str;
    str.resize(len);       // 必须先开空间!
    ifs.read(&str[0], len);

    cout << "读出字符串:" << str << endl;

    ifs.close();
}

// ================== 主函数测试 ==================
int main() {
    writeStr("test.bin", s);
    readStr("test.bin");
    return 0;
}

3. 模板

  • 泛型编程:即与具体类型无关的通用编程;用一套代码,适配所有类型,只关注算法逻辑,不关心数据类型;泛型编程主要依靠模板(template)实现;典型应用有STL 标准库、通用算法、通用容器、通用工具函数等;
  • 模板的学习并不是为了直接使用模板编程,而是在STL 能够运用系统提供的模板;

模板即通用代码模具,让一段代码(函数 / 类)支持任意类型,只写一次,到处通用,提高代码复用性;模板分为函数模板(通用函数)类模板(通用类)

3.1 函数模板

函数模板(function template),定义一个 “通用模具”,让同一个函数逻辑支持任意类型,代码只写一遍;它只适用于函数体相同、形参个数相同而类型不同的情况;

  • 函数模板注意事项
    • 自动推导时,所有用到 T 的地方,类型必须完全一样,不允许自动隐式转换;(这也是与普通函数不同的地方)
    • 只要编译器无法自动推导出 T,就必须手动显式指定 T 的类型;
// 例程:
- **函数模板注意事项**1. 自动推导时,所有用到 `T` 的地方,类型必须完全一样,不允许自动隐式转换:
	template<typename T>
	T add(T a, T b) { // 返回值、a、b 必须完全一样
	    return a + b;
	}

	2. 只要编译器无法自动推导出 `T`,就必须手动显式指定 `T` 的类型:
	template<typename T>
	T func() { // 函数返回值使用 T,但参数不用 T
	    return 123;
	}
	
	func();  // ❌ 错误
	func<int>();    // ✅ T = int

  • 同名普通函数和函数模板的处理:如果普通函数和同名函数模板同时存在,编译器优先调用普通函数;如果想强制用模板,可以加 <> 空模板参数列表实现;
int add(int a,int b);         // 普通函数
template<typename T> T add(T a,T b); // 模板

add(1,2);   // 优先走普通函数
// 或
add<>(1,2); // 强制调用模板
  • 函数模板可以重载:多个参数个数不同的模板,会按重载规则匹配;
// 1. 模板1:一个参数
template<class T>
void func(T a)
{
    cout << "模板1:一个参数" << endl;
}

// 2. 模板2:两个参数(模板重载)
template<class T>
void func(T a, T b)
{
    cout << "模板2:两个参数" << endl;
}

// 3. 普通函数(与模板同名,构成重载)
void func(int a)
{
    cout << "普通函数:int" << endl;
}

int main()
{
    func(10);          // 优先普通函数
    func<>(10);        // 强制调用模板1
    func(3.14);        // 调用模板1
    func(1, 2);        // 调用模板2

    return 0;
}

  • 单类型函数模板
#include <iostream>
using namespace std;

// template - 声明创建模板;
// typename - 表明其后面的符号是一种数据类型;也可以写class,效果相同
// T - 通用的数据类型,自定义,通常用大写字母
template <typename T> // 把函数的返回值和形参类型抽象化
T add(T a, T b)  // T 是一个类型占位符
{
    return a + b;
}

int main()
{
    // 编译器会根据传入的参数,自动生成对应版本
    // int
    cout << add(1, 2) << endl; // add<int>(int, int)

    // double
    cout << add(1.1, 2.2) << endl; // add<double>(double, double)

    // 显式指定类型
	cout << add<int>(3, 4) << endl; // add<int>(int, int)

    return 0;
}
  • 多类型函数模板
template <typename T1, typename T2>
void print(T1 a, T2 b)
{
    cout << a << " " << b << endl;
}

int main()
{
    print(10, 3.14);
}

3.2 类模板

把功能相同,数据类型不同的多个类写成一个类模板,实现一类多用,原理类似于函数模板;

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

template<typename  nameType,typename ageType> // 声明一个模板,两个虚拟类型名为nameType 和 ageType ,这里`typename`和`class`都可以用,无区别
class Person
{
private:
	nameType m_name;
	ageType m_age;

public:
	Person(nameType a, ageType b) { m_name = a;m_age = b; } // 构造函数
	void showPerson() {
		cout << this->m_name << " is " << this->m_age << " years old." << endl;
	}
};

int main(void)
{
	Person<string,int> cmp1("TOM", 17); // 定义对象,<>中必须手动指定模板参数,这也是与函数模板的区别
	cmp1.showPerson();
}
  • 类模板中成员函数也可定义在类外部(常用),但需在类模板外定义的成员函数前都加上模板声明,效果同上;
# include<iostream>
using namespace std;
#include<string>

template<typename  nameType,typename ageType> // 声明一个模板,两个虚拟类型名为nameType 和 ageType ,这里`typename`和`class`都可以用,无区别
class Person
{
private:
	nameType m_name;
	ageType m_age;

public:
	Person(nameType a, ageType b) { m_name = a;m_age = b; } // 构造函数
	void showPerson(); // 声明成员函数
};

template<typename nameType, typename ageType> // 类外模板声明
void Person<nameType, ageType>::showPerson() // 类外成员函数定义
{
	cout << this->m_name << " is " << this->m_age << " years old." << endl;
}

int main(void)
{
	Person<string,int> cmp1("TOM", 17); // 定义对象,<>中指定了模板参数,nameType为string,ageType为int
	cmp1.showPerson();
}
  • 类模版可在模板参数列表中有默认参数
template<typename  nameType,typename ageType = int> // 虚拟类型的默认参数为int
class Person
{
private:
	nameType m_name;
	ageType m_age;

public:
	Person(nameType a, ageType b) { m_name = a;m_age = b; }
	void showPerson();
};

template<typename nameType, typename ageType> 
void Person<nameType, ageType>::showPerson()
{
	cout << this->m_name << " is " << this->m_age << " years old." << endl;
}

int main(void)
{
	Person<string> cmp1("TOM", 17); // 定义对象,<>中指定了模板参数时,int 则不用显式指定
	cmp1.showPerson();
}

3.2.1 类模板对象做函数参数

类模板实例化出的对象,可作为函数的传入参数,其传参方式有三种:

  1. (常用)指定传入的类型,只能传一种类型;
  2. 参数模板化,把函数也写成函数模板,自动适配;
  3. (不推荐)整个类模板化;
1. 指定传入的类型:

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

template<class T>
class Person {
public:
    T m_age;
    Person(T age) {
        m_age = age;
    }
};

// 参数是 Person<int> 类型,固定死
void printPerson(Person<int>& p) {
	cout << "Person age: " << p.m_age << endl;
}

int main(void)
{
    Person<int> p(20);
    printPerson(p);   // ✅ 可以
    
    // Person<double> p2(18.5);
    // printPerson(p2); // ❌ 不行,类型不匹配
    return 0;
}
2. 参数模板化:

template<class T>
class Person {
public:
    T m_age;
    Person(T age) {
        m_age = age;
    }
};

template <typename T>
void printPerson(Person<T>& p) {
	cout << "Person age: " << p.m_age << endl;
}

int main(void)
{
    Person<int> p(20);
    printPerson(p);   // ✅ 可以

     Person<double> p2(18.5);
     printPerson(p2); // ✅ 可以
    return 0;
}
3. 整个类模板化:

template<class T>
class Person {
public:
    T m_age;
    Person(T age) {
        m_age = age;
    }
};

template<template<class> class PersonType, class T>
void printPerson(PersonType<T>& p) {
    cout << "Person age: " << p.m_age << endl;
}

int main(void)
{
    Person<int> p(20);
    printPerson(p);   // ✅ 可以

     Person<double> p2(18.5);
     printPerson(p2); // ✅ 可以
    return 0;
}

3.2.2 类模板与继承

类模板也可以继承,其规则为:

  1. 子类继承类模板时,必须指明父类T的类型;因为类模板不实例化就不是真正的类型,继承必须用具体类型;
  2. 子类也写成类模板;
  3. 子类访问父类成员时,建议用 this-> 或 父类名::,避免编译器找不到;
1. 子类继承类模板时,必须指明父类`T`的类型:

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

template<class T>
class Base {
public:
    T m_a;

    Base(T a) {
        m_a = a;
    }
};

class Son : public Base<int> { // 继承时必须指明父类类型,这里为<int>
public:
    Son(int a) : Base<int>(a) {
    }
};

int main(void)
{
    Son s(10);   // ✅ 正常
    return 0;
}
2. 子类也写成类模板:

template<class T>
class Base {
public:
    T m_a;

    Base(T a) {
        m_a = a;
    }
};
template<class T> // 子类也是模板,把 T 传给父类
class Son : public Base<T> {
public:
    Son(T a) : Base<T>(a) {
    }

    void show() {
        std::cout << this->m_a << std::endl; // 访问父类成员需加 this->
    }
};

int main(void)
{
    Son<int> s1(10);
    Son<double> s2(3.14);

    s1.show();
    s2.show();

    return 0;
}

3.3 模板的局限性

模板并不是万能的,它对传入的数据类型有隐含要求,有些类型无法直接使用模板;

  1. 模板依赖类型操作,除非有运算符重载或模板特化操作;
  2. 模板不能隐式类型转换;
  3. 模板声明和实现不能分开写在 .h.cpp
1. 模板依赖类型操作:如下例程,intdouble 可以用,但如果传入自定义结构体、类、数组等,直接编译失败(除非有运算符重载)template<typename T>
	bool compare(T a, T b) {
	    return a == b;
	}

解决上述问题,可以用到模板特化:
- 模板特化:给某些特定类型,单独写一套不一样的模板实现;
	// 1. 定义一个类
	class Person {
	public:
	    string name;
	    int age;
	
	    Person(string name, int age) {
	        this->name = name;
	        this->age = age;
	    }
	};
	
	// 2. 模板特化(具体化):专门处理 Person 类
	template<>
	bool compare<Person>(Person a, Person b) {
	    // 自定义比较规则:名字和年龄都相同才算相等
	    return (a.name == b.name) && (a.age == b.age);
	}
	
	int main() {
	    Person p1("张三", 20);
	    Person p2("张三", 20);
	
	    compare(10, 20);     // 调用通用模板
	    compare(p1, p2);     // 调用【类的特化版本】
	
	    return 0;
	}

子类对象对其父类对象的赋值(赋值兼容)

与不同数据类型数据变量可相互赋值一样,不同对象之间也可以赋值,前面已讲过对象的赋值、克隆,对于公有继承的子类对象也可以对其父类对象进行赋值,赋值时子类对象的数据成员全部赋值给其父类对象,而子类新增的成员不进行赋值;

  • 子类对象对父类对象的引用进行赋值或初始化
A a1; // 定义父类A的对象a1 
B b1; // 定义公有继承子类B的对象b1 
A &r=a1; //定义父类A的对象引用r,并用其子类对象a1对其进行初始化,表示r是a1的引用(别名),r和a1共享一段存储单元

也可以用其子类对象对r进行初始化,把上面最后一行A &r=a1;改为
A &r=b1; 

也可以对r重新赋值(保留A &r=a1;)
r=b1; 

用类对象作为类成员数据(类的组合)

在类中可用对象作为成员数据,即子对象,它可以是本类的父类对象也可以是另一个类对象;

class Teacher 
{
	public:
		...
	
	private:
		int num;
		string name;
		char sex;
};

class BirthDate
{
	public:
		...
		
	private:
		int year;
		int month;
		int day;
};
 
class Professor : public Teacher
{
	public:
		...
	
	private:
		BirthDate birthday; // 以BirthDate类对象作为成员数据 
};

结果Professor类从Teacher类中得到num,name,sex等成员数据,从BirthDate类中得到year,month,day等成员数据 


Professor prof1; // 定义类对象 

有两函数: 
void fun1(Teacher &); // 函数形参为Teacher类对象的引用 
void fun2(BirthDate &); // 函数形参为BirthDate类对象的引用 

在main函数中调用函数时 
fun1(prof1); // 合法,实参为Teacher类的子类对象 
fun2(prof1.birthday); // 合法,实参与形参类型对应 
fun2(prof1); // 非法,实参是Professor类,形参与实参不匹配 

继承与派生

声明继承

  • 注意
    • 子类接收父类的全部成员(构造函数、析构函数除外),此步骤不具选择性;
    • 可通过继承方式可改变父类成员在子类中的访问属性;
    • 可通过在子类中声明与父类成员同名的成员,进而覆盖取代它,若成员函数,为避免重载,应使函数名和参数表都相同;
语法:
class 子类名 : 继承方式 父类名
{
	子类新增内容 
 }; 

例程:
# include<iostream>
#include<string>
using namespace std;

/* ---- 类 ---- */ 
class Human
{
public:
	Human(int,string);
	void displayInfo();

protected:
	int age;
	string add;
};

class Man : public Human
{
public:
	// 构造函数 
	Man(string nation):Human(age,add){nation=nation;}; 
	void displayInfo();

private:
	string nation;
 };

/* ---- 构造函数 ---- */ 
Human::Human(int age,string add)
{
	age=age;
	add=add;
}

/* ---- 成员函数 ---- */ 
void Human::displayInfo()
{
	cout<<"age: "<<age<<endl;
	cout<<"address: "<<add<<endl;
}

void Man::displayInfo()
{
    cout<<"age: "<<age<<endl;
	cout<<"address: "<<add<<endl;
	cout<<"nation: "<<nation<<endl;
}

int main(void)
{
	return 0;
}

子类成员的访问属性

情况 处理原则
自己访问自己的成员 私有成员只能被同类成员函数访问,公用成员可被外界函数访问
父类成员函数访问子类成员 不允许
子类成员函数访问父类成员
外界函数访问子类成员 只可访问公用成员
外界函数访问父类成员 /

子类的构造函数与析构函数

父类的构造函数与析构函数不能被子类继承,而子类新增的成员数据需要子类自己的构造函数进行初始化,实际上,在子类调用构造函数时,其父类的构造函数也同时被调用,完成了对子类从父类中继承的成员数据和其新增成员数据的初始化;

多重继承

  • 声明多重继承:以下例程表示类D以公用继承方式继承类A, 以私有继承方式继承类B, 以受保护继承方式继承类C;
语法:
class D:public A,private B,protected C
{
	...
};
  • 多重继承子类的构造函数:类似于单继承子类的构造函数;
例程:
Man(string nation):Human(age,add),Animal(type) 
{
	...
}; 

多态性与虚函数

  • 面向对象的多态性(Polymorphism):同一消息在不同对象接收时会产生不同的行为,即每个对象用自己的方式响应同一消息;

多态性极大程度地降低了编程工作负担,提高编程效率,就好比学校向外界发布开学消息,不同的对象接收到消息这一消息后会作出不同的响应,学生准备上学,家长准备学费,教师备课等,如果没有多态性,那么学校就要给不同的对象发不同的消息,布置不同的任务;

静态/动态多态性

面向对象编程的多态性的表现形式之一是让具有不同功能的函数赋予同一函数名,即函数重载;

  • 静态多态性:通过重载实现,程序在编译时就知道调用函数的全部信息,系统就知道要调用哪个函数;
  • 动态多态性:在程序运行过程中动态地确认调用哪个函数,通过**虚函数(virtual function)**实现;

------------------------

避免程序运行后窗口自动关闭


在主调函数中return语句前添加cin.get();

int main(void)
{
	cin.get();  // 避免窗口自动关闭 
	return 0;
}

异常处理


即事先分析程序运行时可能出现的异常情况,并分别制定处理方法,如拨打电话时拨了一个空号,系统提示此号码为空号,而不是无休止地等待;
异常不完全等于出错,异常是出现与人们期望不同的情况;

异常处理的方法

  • 检查(try):把需要检查的语句放入try语块中;
  • 捕捉(catch):当try语块出现异常时,捕捉异常信息,并处理异常;
  • 抛出(throw):当try语块出现异常时,发出一个异常信息(给上一级函数);

throwcatch是独立同步进行的;
try-catch语句作为整体使用,就像if-else一样,当然也可以只有try,就像只有if
try-catch语句中try只能有一个,但catch可有多个,便于与不同异常信息进行匹配处理;

  • catch语法
1. 只指定需要捕捉的异常信息类型 
catch(double)
catch(char)
2. 指定需要捕捉的异常信息类型和变量名
catch(double a)
catch(char b) 
3. ...,表示捕捉任何类型的异常信息,一般放到所有catch的最后,相当于“其他” 
catch(...)
4. 把处理异常的任务交给上一级函数
catch(int) 
{
	throw;
}
  • 例程
#include <iostream>
#include <cmath>

using namespace std;

int main()
{
	double triangleArea(double,double,double);
	double a,b,c;
	
	cout<<"请输入三角形的三边长.(Please typing three side length of the triangle.)"<<endl;
	cin>>a>>b>>c;
	try
	{
		while(a>0 && b>0 && c>0)
		{
			cout<<"此三角形的面积为:(Area of this triangle is:)"<<"\n"<<triangleArea(a,b,c)<<endl;
			cout<<"请继续输入:(Please go on typing:)"<<endl;
			cin>>a>>b>>c; 
		}
	}	
	catch(double) // 若 try语块中发生异常,则跳到此 catch语块 
	{
		cout<<"a="<<a<<",b="<<b<<",c="<<c<<",报错!这不是一个三角形!(Error!that is not a triangle!)"<<endl;
	}
	cout<<"程序结束.(program exit.)"<<endl;
	return 0;
}

double triangleArea(double a,double b,double c)  
{
	if (a+b<=c||b+c<=a||c+a<=b)throw a; // 执行完 throw语句,立即结束并离开本函数
	double s=(a+b+c)/2;
	return sqrt(s*(s-a)*(s-b)*(s-c));
}

// throw 抛出的a是double型,catch捕捉的异常也是double型,两者类型匹配;

在这里插入图片描述

在函数声明时指定只能抛出的异常类型

double triangleArea(double,double,double) throw(double);// 该函数只能抛出double类型异常信息;
double triangleArea(double,double,double) throw(double,int,char,float);// 该函数能抛出double,intd等类型异常信息
double triangleArea(double,double,double) throw();// 该函数不能抛出异常信息;

C++头文件


在C++中可以用C语言中的所有头文件,且在C语言库的基础上做了较大改变,为了区分C语言的库,新式C++的头文件不采用后缀.h,而C语言的头文件保留了后缀它,如C语言中数学函数库为math.h在C++中作改变后命名其为cmath

不需.h后缀的前提是使用std命名空间using namespace std;

在这里插入图片描述

命名空间


  • 作用一:相当于对某些代码取别名;
  • 作用二:用于处理程序中常见的命名冲突;
1. 使用命名空间
#include<iostream>
using namespace std;//std是C++中的标准命名空间

cout<<"age: "<<age<<endl;

2. 不使用命名空间
#include<iostream.h>

std::cout<<"age: ";
std::cout<<age;
std::cout<<endl;

综上,命名空间把std::cout取别名cout,iostream.h命名为iostream,代码更加高效直观;

类和动态内存分配


动态内存分配即在程序运行时按需为数据分配内存,使用完马上释放,而不是在编程的时候把数据的内存大小、建立和释放时间写死;

参考连接

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐