C++语法

文章目录

  • C++面向对象程序设计
    • 一.C++语言与OO思想介绍
    • 二.C++基础
      • 2.1 输入与输出
      • 2.2 const修饰符
      • 2.3 void型指针
      • 2.4 内联函数
      • 2.5 带有默认参数值的函数
      • 2.6 函数重载
      • 2.7 函数覆盖
      • 2.8 作用域标识符"::"
      • 2.9 强制类型转换
      • 2.10 new和delete运算符
      • 2.11 引用
    • 三.类和对象(一)
      • 3.1 类的构成
      • 3.2 成员函数的定义
      • 3.3 函数的定义与调用
      • 3.4 构造函数与析构函数
    • 四.类和对象(二)
      • 4.1 自引用指针this
      • 4.2 对象数组与对象指针
      • 4.3 string类
      • 4.4 向函数传递对象
      • 4.5 静态成员
      • 4.6 友元
      • 4.7 类的组合
    • 五.继承和派生
      • 5.1 继承和派生的概念
      • 5.2 派生类的构造函数与析构函数
      • 5.3 多继承
    • 六.多态性与虚函数
      • 6.1 多态性概述
      • 6.2 虚函数
      • 6.3 虚析构函数
      • 6.4 纯虚函数
      • 6.5 抽象类
    • 七.运算符重载
    • 八.函数模板与类模板
      • 8.1 函数模板
      • 8.2 类模板
    • 九.C++的输入与输出
      • 9.1 C++为何建立自己的输入/输出系统
      • 9.2 C++的流库及其输出结构
      • 9.3 预定的流对象
      • 9.4 输入/输出流的成员函数
      • 9.5 文件输入/输出
    • 十.异常处理与命名空间
      • 10.1 异常处理
      • 10.2 命名空间和头文件命名规则
    • 十一.STL标准模板库
      • 11.1Vector
      • 11.2 list容器

C++面向对象程序设计

Date:2023/11.30

Editor:阮扬

一.C++语言与OO思想介绍

C++是一种通用的、静态类型的编程语言,它结合了高级语言的特性和底层编程的能力。C++是在C语言基础上发展而来的,它引入了面向对象(Object-Oriented,OO)的思想,使得程序的设计更加模块化、可扩展和易于维护。

面向对象(OO)是一种软件开发方法,它将现实世界中的对象和概念映射到程序设计中。面向对象的设计思想强调将问题分解为一组相互协作的对象,每个对象都有自己的状态(属性)和行为(方法)。在C++中,面向对象的特性通过类和对象的概念来实现。

以下是C++语言和面向对象思想的一些关键特性和概念:

  1. 类和对象:类是一个模板或蓝图,描述了对象的属性和行为。对象是类的一个实例,具有类定义的属性和行为。类定义了对象的结构和行为,对象是根据类的定义创建的实体。

  2. 封装:封装是将数据和操作封装在一个单元(类)中,通过限制对数据的直接访问,保护数据的完整性和安全性。通过封装,对象的内部实现细节对外部是不可见的,只提供了公共接口供其他对象进行交互。

  3. 继承:继承是一种机制,允许一个类继承另一个类的属性和行为。通过继承,一个类(子类)可以从另一个类(父类)继承属性和方法,并可以添加自己的特定功能。继承可以实现代码的重用和层次化的组织结构。

  4. 多态:多态是指同一种操作或函数可以在不同的对象上产生不同的行为。它允许以统一的方式处理不同类型的对象,提高了代码的灵活性和可扩展性。C++中的多态性通过虚函数和函数重写(override)来实现。

  5. 抽象类和接口:抽象类是一个不能实例化的类,它定义了一组纯虚函数(没有实现的函数),作为其他类的基类。接口是一种特殊的抽象类,它只包含纯虚函数。抽象类和接口提供了一种规范和约束,用于定义类的行为和功能。

C++语言通过引入面向对象的思想,提供了类、对象、封装、继承、多态等特性,使得程序的设计更加模块化、可维护和可扩展。面向对象的设计思想强调将问题分解为一组相互协作的对象,使得代码更易于理解、重用和维护。在C++中,可以使用类和对象来组织和管理代码,通过继承和多态实现代码的灵活性和可扩展性。

二.C++基础

2.1 输入与输出

int i;
float f;
cin >> i;
cout << f<> a >> b >> c;

输入:cin

  • 在默认情况下,运算符>>将跳过空白符,然后读入后面与变量类型相对应的值。因此,给一组变量输入值时可用空格符、回车符、制表符将输入的数据间隔开。
  • 当输入字符串(即类型为string的变量)时,提取运算符“>>”的作用是跳过空白字符,读入后面的非空白字符,直到遇到另一个空白字符为止,并在串尾放一个字符串结束标志‘\0’。

输出:cout

  • 通过使用<<操作符,可以将文本、变量、表达式插入到cout中进行输出

换行:endl

  • endl是 C++ 标准库中的一个流操作符,用于在输出流中插入换行符并刷新流。它的作用类似于插入 \n 字符,但它还会执行刷新操作。

2.2 const修饰符

在C语言中,习惯使用#define来定义常量,例如#define PI 3.14,C++提供了一种更灵活、更安全的方式来定义常量,即使用const修饰符来定义常量。例如const float PI = 3.14

const可以与指针一起使用,它们的组合情况复杂,可归纳为3种:指向常量的指针常指针指向常量的常指针

常量的指针

当为常量指针时,不可以通过修改所指向的变量的值,但是指针可以指向别的变量。

int a = 5;
const int *p =&a;
*p = 20;   //error  不可以通过修改所指向的变量的值

int b =20;
p = &b; //right  指针可以指向别的变量

常指针

当为指针常量时,指针常量的值不可以修改,就是不能指向别的变量,但是可以通过指针修改它所指向的变量的值。

int a = 5;
int *const p = &a;
*p = 20;     //right 可以修改所指向变量的值

int b = 10;
p = &b;      //error 不可以指向别的变量

指向常量的常指针

这个指针所指的地址不能改变,它所指向的地址中的内容也不能改变

const char* const pc = "abcd";
内容和地址均不能改变

说明:

  • 如果用const定义整型常量,关键字可以省略。即 const in bufsize = 100 与 const bufsize = 100等价;

  • 常量一旦被建立,在程序的任何地方都不能再更改。

  • 与#define不同,const定义的常量可以有自己的数据类型。

  • 函数参数也可以用const说明,用于保证实参在该函数内不被改动。

2.3 void型指针

void通常表示无值,但将void作为指针的类型时,它却表示不确定的类型。这种void型指针是一种通用型指针,也就是说任何类型的指针值都可以赋给void类型的指针变量。

需要指出的是,这里说void型指针是通用指针,是指它可以接受任何类型的指针的赋值,但对已获值的void型指针,对它进行再处理,如输出或者传递指针值时,则必须再进行显式类型转换,否则会出错。

    void* pc;
    int i = 123;
    char c = 'a';
    pc = &i;
	cout << pc << endl;         //输出指针地址006FF730
	cout << *(int*)pc << endl;  //输出值123
    pc = &c;
	cout << *(char*)pc << endl; //输出值a

2.4 内联函数

内联函数是一种在编译时将函数调用处替换为函数体的机制。它是一种优化技术,旨在减少函数调用的开销,提高程序的执行效率

通常情况下,函数的调用过程包括保存当前函数的上下文、跳转到被调用函数、执行被调用函数的代码、返回到调用函数的位置等步骤。这些步骤会引入一定的开销,尤其是对于频繁调用的小型函数而言。

使用内联函数可以避免函数调用的开销。当一个函数被声明为内联函数时,编译器会将函数的定义体嵌入到每个调用处,而不是生成函数调用指令。这样可以减少函数调用的开销,并且可以在编译时进行更多的优化。

要声明内联函数,通常在函数定义前加上 inline 关键字。例如:

inline int add(int a, int b) {
    return a + b;
}
image-20231130163552432

说明

  • 内联函数在第一次被调用之前必须进行完整的定义,否则编译器将无法知道应该插入什么代码
  • 在内联函数体内一般不能含有复杂的控制语句,如for语句和switch语句等
  • 使用内联函数是一种空间换时间的措施,若内联函数较长,较复杂且调用较为频繁时不建议使用

2.5 带有默认参数值的函数

当进行函数调用时,编译器按从左到右的顺序将实参与形参结合,若未指定足够的实参,则编译器按顺序用函数原型中的默认值来补足所缺少的实参。

void init(int x = 5, int y = 10);
init (100, 19);   // 100 , 19
init(25);         // 25, 10
init();           // 5, 10
  • 在函数原型定义中,所有取默认值的参数都必须出现在不取默认值的参数的右边

    int fun(int a, int b, int c = 111);
    
  • 在函数调用时,若某个参数省略,则其后的参数皆应省略而采取默认值。不允许某个参数省略后,再给其后的参数指定参数值。

2.6 函数重载

在C++中,用户可以重载函数。这意味着,在同一作用域内,只要函数参数的类型不同,或者参数的个数不同,或者二者兼而有之,两个或者两个以上的函数可以使用相同的函数名。

#include 
using namespace std;

int add(int x, int y)
{
	return x + y;
}

double add(double x, double y)
{
	return x + y;
}

int add(int x, int y, int z)
{
	return x + y + z;
}

int main() 
{
	int a = 3, b = 5, c = 7;
	double x = 10.334, y = 8.9003;
	cout << add(a, b) << endl;
	cout << add(x, y) << endl;
	cout << add(a, b, c) << endl;
	return 0;
}

注意:

  • 调用重载函数时,函数返回值类型不在参数匹配检查之列。因此,若两个函数的参数个数和类型都相同,而只有返回值类型不同,则不允许重载。
int mul(int x, int y);
double mul(int x, int y);
  • 函数的重载与带默认值的函数一起使用时,有可能引起二义性。
void Drawcircle(int r = 0, int x = 0, int y = 0);
void Drawcircle(int r);
Drawcircle(20);
  • 在调用函数时,如果给出的实参和形参类型不相符,C++的编译器会自动地做类型转换工作。如果转换成功,则程序继续执行,在这种情况下,有可能产生不可识别的错误。
void f_a(int x);
void f_a(long x);
f_a(20.83);

2.7 函数覆盖

函数的覆盖(override)是面向对象编程中的一个概念,用于指定派生类中的函数重写基类中的同名函数。通过函数的覆盖,可以实现多态性,即在运行时根据对象的实际类型选择调用对应的函数。

在 C++ 中,函数的覆盖需要满足以下条件:

  1. 基类和派生类之间存在继承关系。
  2. 基类和派生类中的函数具有相同的名称。
  3. 基类中的函数必须声明为虚函数(virtual)。

下面是一个简单的示例代码,演示了函数的覆盖:

#include 

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

int main() {
    Shape* shape = new Circle();
    shape->draw();  // 输出:Drawing a circle.

    delete shape;

    return 0;
}

在上面的示例中,Shape 是基类,Circle 是派生类。基类中定义了一个虚函数 draw,派生类中重写了这个函数。在 main 函数中,创建了一个指向 Circle 对象的基类指针 shape,然后调用 shape->draw()。由于 draw 函数是虚函数,并且指针 shape 指向的实际对象是 Circle 类型,因此会调用派生类中的 draw 函数,输出 “Drawing a circle.”。

通过函数的覆盖,可以实现多态性。在运行时,根据对象的实际类型来选择调用的函数,而不是根据指针或引用的静态类型。这样可以使程序更加灵活和可扩展,提高代码的可维护性和可重用性。

需要注意的是,为了确保函数的正确覆盖,建议在派生类中的函数声明中使用 override 关键字进行标记。这样可以帮助编译器检查函数的正确性,避免由于函数签名不匹配等问题导致的错误。

2.8 作用域标识符"::"

作用域标识符 “::” 在 C++ 中用于指定命名空间、类、结构体、枚举等标识符的作用域。它可以帮助管理标识符的命名空间和作用域,以避免命名冲突和歧义。

  1. 命名空间的作用域标识符:在命名空间中定义的变量、函数、类等标识符可以使用命名空间作用域标识符来访问。例如:
namespace MyNamespace {
    int x;
    void foo() {
        // 通过作用域标识符访问命名空间中的变量
        MyNamespace::x = 10;
    }
}
  1. 类的作用域标识符:在类中定义的成员变量、成员函数可以使用类作用域标识符来访问。例如:
class MyClass {
public:
    int x;
    void foo() {
        // 通过作用域标识符访问类中的成员变量
        MyClass::x = 10;
    }
};
  1. 嵌套类的作用域标识符:在一个类中定义的嵌套类可以使用作用域标识符来访问。例如:
class Outer {
public:
    class Inner {
    public:
        void foo() {
            // 通过作用域标识符访问嵌套类中的成员函数
        }
    };
};
  1. 枚举的作用域标识符:在枚举类型中定义的枚举值可以使用作用域标识符来访问。例如:
enum MyEnum {
    VALUE1,
    VALUE2
};

void foo() {
    // 通过作用域标识符访问枚举值
    MyEnum value = MyEnum::VALUE1;
}

通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权,它将屏蔽全局变量。

如果希望在局部变量的作用域内使用同名的全局变量,可以在该变量前加上“::”,此时::value代表全局变量value,“::”称为作用域标识符。

#include 
using namespace std;

int value;   //定义全局变量value

int main() 
{
	int value;  //定义局部变量value
	value = 100;
	::value = 1000;
	cout << "local value : " << value << endl;
	cout << "global value : " << ::value << endl;
	return 0;
}

2.9 强制类型转换

可用强制类型转换将不同类型的数据进行转换。例如,要把一个整型数(int)转换为双精度型数(double),可使用如下的格式:

int i = 10;
double x = (double)i;
或
int i = 10;
double x = double(i);

以上两种方法C++都能接受,建议使用后一种方法。

2.10 new和delete运算符

程序运行时,计算机的内存被分为4个区:程序代码区、全局数据区、堆和栈。其中,堆可由用户分配和释放。C语言中使用函数malloc()free()来进行动态内存管理。C++则提供了运算符newdelete来做同样的工作,而且后者比前者性能更优越,使用更灵活方便。

指针变量名 = new 类型
    int *p;
    p = new int;
delete 指针变量名
    delete p;

下面对newdelete的使用再做一下几点说明:

  • 用运算符new分配的空间,使用结束后应该用也只能用delete显式地释放,否则这部分空间将不能回收而变成死空间。

  • 在使用运算符new动态分配内存时,如果没有足够的内存满足分配要求,new将返回空指针(NULL)。

  • 使用运算符new可以为数组动态分配内存空间,这时需要在类型后面加上数组大小。

指针变量名 = new 类型名[下标表达式];
int *p = new int[10];

释放动态分配的数组存储区时,可使用delete运算符。

delete []指针变量名;
delete p;
  • new 可在为简单变量分配空间的同时,进行初始化
指针变量名 = new 类型名(初值);
int *p;
p = new int(99);
···
delete p;

2.11 引用

引用(reference)是C++对C的一个重要扩充。变量的引用就是变量的别名,因此引用又称别名

类型 &引用名 = 已定义的变量名

引用与其所代表的变量共享同一内存单元,系统并不为引用另外分配存储空间。实际上,编译系统使引用和其代表的变量具有相同的地址。

#include 
using namespace std;
int main() 
{
	int i = 10;
	int &j = i;
	cout << "i = " << i << " j = " << j << endl;
	cout << "i的地址为 " << &i << endl;
	cout << "j的地址为 " << &j << endl;
	return 0;
}

上面代码输出i和j的值相同,地址也相同。

  • 引用并不是一种独立的数据类型,它必须与某一种类型的变量相联系。在声明引用时,必须立即对它进行初始化,不能声明完成后再赋值。
int& d;//错误,未初始化
  • 为引用提供的初始值,可以是一个变量或者另一个引用。
  • 指针是通过地址间接访问某个变量,而引用则是通过别名直接访问某个变量。

引用作为函数参数、使用引用返回函数值

#include 
using namespace std;

void swap(int &a, int &b)
{
	int t = a;
	a = b;
	b = t;
}

int a[] = {1, 3, 5, 7, 9};

int& index(int i)
{
	return a[i];
}

int main() 
{
	int a = 5, b = 10;
	//交换数字a和b
	swap(a, b);
	cout << "a = " << a << " b = " << b << endl;
	cout << index(2) << endl;   //等价于输出元素a[2]的值
	index(2) = 100;             //等价于将a[2]的值赋为100;
	cout << index(2) << endl;
	
	return 0;
}

对引用进一步说明

  • 不允许建立void类型的引用
  • 不能建立引用的数组
  • 不能建立引用的引用。不能建立指向引用的指针。引用本身不是一种数据类型,所以没有引用的引用,也没有引用的指针。
  • 可以将引用的地址赋值给一个指针,此时指针指向的是原来的变量。
  • 可以用const对引用加以限定,不允许改变该引用的值,但是它不阻止引用所代表的变量的值。

三.类和对象(一)

3.1 类的构成

类声明中的内容包括数据和函数,分别称为数据成员和成员函数。按访问权限划分,数据成员和成员函数又可分为共有、保护和私有3种。

class 类名{
    public:
        公有数据成员;
        公有成员函数;
    protected:
        保护数据成员;
        保护成员函数;
    private:
        私有数据成员;
        私有成员函数;
};
  • 对一个具体的类来讲,类声明格式中的3个部分并非一定要全有,但至少要有其中的一个部分。一般情况下,一个类的数据成员应该声明为私有成员,成员函数声明为共有成员。这样,内部的数据整个隐蔽在类中,在类的外部根本就无法看到,使数据得到有效的保护,也不会对该类以外的其余部分造成影响,程序之间的相互作用就被降低到最小。
  • 类声明中的关键字privateprotectedpublic可以任意顺序出现。
  • 若私有部分处于类的第一部分时,关键字private可以省略。这样,如果一个类体中没有一个访问权限关键字,则其中的数据成员和成员函数都默认为私有的。
  • 不能在类声明中给数据成员赋初值。

3.2 成员函数的定义

普通成员函数定义

在 C++ 中,普通成员函数是定义在类中的成员函数,它们可以访问类的成员变量和其他成员函数。普通成员函数可以在类的内部声明(隐式声明)和定义,或者在类的外部定义(显式声明)。

下面是普通成员函数的定义语法:

class MyClass {
public:
    // 在类的内部声明和定义普通成员函数
    void memberFunction() {
        // 函数体
    }
};

// 在类的外部定义普通成员函数
void MyClass::memberFunction() {
    // 函数体
}

在类的内部定义普通成员函数时,函数的定义直接写在类的定义中,函数体在花括号内部。这种方式可以直接访问类的成员变量和其他成员函数,不需要使用作用域标识符来指定类的作用域。

在类的外部定义普通成员函数时,需要使用作用域标识符指定类的作用域,然后再写出函数的定义。这种方式适用于在类的外部分离定义函数的情况,可以将函数的声明和定义分开,提高代码的可读性和可维护性。

需要注意的是,普通成员函数的定义必须在类的定义之后,否则编译器无法识别类的成员函数。可以将类的定义放在头文件中,然后在源文件中包含头文件,以便在源文件中定义普通成员函数。

普通成员函数可以通过类的对象来调用,例如:

MyClass obj;
obj.memberFunction();  // 调用普通成员函数

内联成员函数的定义

  • 隐式声明:将成员函数直接定义在类的内部
class Score{
public:
	void setScore(int m, int f)
	{
		mid_exam = m;
		fin_exam = f;
	}
	void showScore()
	{
		cout << "期中成绩: " << mid_exam << endl;
		cout << "期末成绩:" << fin_exam << endl;
	}
private:
	int mid_exam;
	int fin_exam;
};
  • 显式声明:在类声明中只给出成员函数的原型,而将成员函数的定义放在类的外部。
class Score{
public:
	inline void setScore(int m, int f);
	inline void showScore();
private:
	int mid_exam;
	int fin_exam;
};

inline void Score::setScore(int m, int f) 
{
	mid_exam = m;
	fin_exam = f;
}

inline void Score::showScore()
{
	cout << "期中成绩: " << mid_exam << endl;
	cout << "期末成绩:" << fin_exam << endl;
}

说明:在类中,使用inline定义内联函数时,必须将类的声明和内联成员函数的定义都放在同一个文件(或同一个头文件)中,否则编译时无法进行代码置换。

3.3 函数的定义与调用

通常把具有共同属性和行为的事物所构成的集合称为类。

类的对象可以看成该类类型的一个实例,定义一个对象和定义一个一般变量相似。

对象的定义

  • 在声明类的同时,直接定义对象
class Score{
public:
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
}op1, op2;
  • 声明了类之后,在使用时定义对象
  Score op1, op2;

对象成员的访问

对象名.数据成员名对象名.成员函数名[(参数表)]op1.setScore(89, 99);
op1.showScore();

说明

  • 在类的内部所有成员之间都可以通过成员函数直接访问,但是类的外部不能访问对象的私有成员。
  • 在定义对象时,若定义的是指向此对象的指针变量,则访问此对象的成员时,不能用“.”操作符,而应该使用“->“操作符。如
	Score op, *sc;
	sc = &op;
	sc->setScore(99, 100);
	op.showScore();

类的作用域和类属性的访问属性

私有成员只能被类中的成员函数访问,不能在类的外部通过类的对象进行访问。

class MyClass {
private:
    int privateVariable;  // 私有成员变量

    void privateFunction() {
        // 私有成员函数
    }

public:
    void publicFunction() {
        // 公有成员函数
        privateVariable = 10;  // 在类的内部可以访问私有成员变量
        privateFunction();    // 在类的内部可以调用私有成员函数
    }
};

int main() {
    MyClass obj;
    obj.publicFunction();  // 在类的外部可以调用公有成员函数
    // obj.privateVariable = 20;  // 错误!无法直接访问私有成员变量
    // obj.privateFunction();    // 错误!无法直接调用私有成员函数
    return 0;
}

一般来说,公有成员是类的对外接口,而私有成员是类的内部数据和内部实现,不希望外界访问。将类的成员划分为不同的访问级别有两个好处:一是信息隐蔽,即实现封装,将类的内部数据与内部实现和外部接口分开,这样使该类的外部程序不需要了解类的详细实现;二是数据保护,即将类的重要信息保护起来,以免其他程序进行不恰当的修改。

级别 允许谁来访问
public 任何代码
protected 这个类本身和它的子类
private 只有这个类本身

对象赋值语句

Score op1, op2;
op1.setScore(99, 100);
op2 = op1;
op2.showScore();

3.4 构造函数与析构函数

构造函数

构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。构造函数的名字必须与类名相同,而不能由用户任意命名。它可以有任意类型的参数,但不能具有返回值。它不需要用户来调用,而是在建立对象时自动执行。

class Score{
public:
	Score(int m, int f);  //构造函数
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
}

在建立对象的同时,采用构造函数给数据成员赋值,通常由以下两种形式

  • 类名 对象名[(实参表)]
    Score op1(99, 100);
    op1.showScore();
    
  • 类名 *指针变量名 = new 类名[(实参表)]
    Score *p;
    p = new Score(99, 100);
    p->showScore();
    -----------------------
    Score *p = new Score(99, 100);
    p->showScore();
    

说明:

  • 构造函数的名字必须与类名相同,否则编译程序将把它当做一般的成员函数来处理。
  • 构造函数没有返回值,在定义构造函数时,是不能说明它的类型的。
  • 与普通的成员函数一样,构造函数的函数体可以写在类体内,也可写在类体外。
  • 构造函数一般声明为共有成员,但它不需要也不能像其他成员函数那样被显式地调用,它是在定义对象的同时被自动调用,而且只执行一次。
  • 构造函数可以不带参数。

成员初始化列表

成员初始化列表是在 C++ 中用于在构造函数中初始化类成员的一种方式。它可以在构造函数的定义中使用冒号(:)后面的成员初始化列表来初始化类的成员变量。

类名::构造函数名([参数表])[:(成员初始化列表)]
{
    //构造函数体
}
class A{
private:
	int x;
	int& rx;
	const double pi;
public:
	A(int v) : x(v), rx(x), pi(3.14)    //成员初始化列表
	{	}
	void print()
	{
		cout << "x = " << x << " rx = " << rx << " pi = " << pi << endl;
	}
};

使用成员初始化列表可以在构造函数创建对象时直接初始化成员变量,而不是在构造函数体内进行赋值操作。这样可以提高代码的执行效率,并且在一些情况下,必须使用成员初始化列表来初始化类的成员变量,例如对于 const 成员变量或引用类型的成员变量。

析构函数

析构函数也是一种特殊的成员函数。它执行与构造函数相反的操作,通常用于撤销对象时的一些清理任务,如释放分配给对象的内存空间等。析构函数有以下一些特点:

  • 析构函数与构造函数名字相同,但它前面必须加一个波浪号(~)。
  • 析构函数没有参数和返回值,也不能被重载,因此只有一个。
  • 当撤销对象时,编译系统会自动调用析构函数。
class Score{
public:
	Score(int m = 0, int f = 0);
	~Score();       //析构函数
private:
	int mid_exam;
	int fin_exam;
};

Score::Score(int m, int f) : mid_exam(m), fin_exam(f)
{
	cout << "构造函数使用中..." << endl;
}

Score::~Score()
{
	cout << "析构函数使用中..." << endl;
}

**说明:**在以下情况中,当对象的生命周期结束时,析构函数会被自动调用:

  • 如果定义了一个全局对象,则在程序流程离开其作用域时,调用该全局对象的析构函数。
  • 如果一个对象定义在一个函数体内,则当这个函数被调用结束时,该对象应该被释放,析构函数被自动调用。
  • 若一个对象是使用new运算符创建的,在使用delete运算符释放它时,delete会自动调用析构函数。

如下示例:

#include 
#include 

using namespace std;

class Student{
private:
	char *name;
	char *stu_no;
	float score;
public:
	Student(char *name1, char *stu_no1, float score1);
	~Student();
	void modify(float score1);
	void show();
};

Student::Student(char *name1, char *stu_no1, float score1)
{
	name = new char[strlen(name1) + 1];
	strcpy(name, name1);
	stu_no = new char[strlen(stu_no1) + 1];
	strcpy(stu_no, stu_no1);
	score = score1;
}

Student::~Student() 
{
	delete []name;
	delete []stu_no;
}

void Student::modify(float score1) 
{
	score = score1;
}

void Student::show()
{
	cout << "姓名: " << name << endl;
	cout << "学号: " << stu_no << endl;
	cout << "成绩:" << score << endl;
}

int main()
{
	Student stu("雪女", "2020199012", 99);
	stu.modify(100);
	stu.show();

	return 0;
}

默认的构造函数和析构函数

如果没有给类定义构造函数,则编译系统自动生成一个默认的构造函数。

  • 对没有定义构造函数的类,其公有数据成员可以用初始值列表进行初始化。
class A{
public:
	char name[10];
	int no;
};

A a = {"chen", 23};
cout << a.name << a.no << endl;
  • 只要一个类定义了一个构造函数(不一定是无参构造函数),系统将不再给它提供默认的构造函数。

每个类必须有一个析构函数。若没有显示地为一个类定义析构函数,编译系统会自动生成一个默认的析构函数。

构造函数的重载

class Score{
public:
	Score(int m, int f);  //构造函数
	Score();
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
};

**注意:**在一个类中,当无参数的构造函数和带默认参数的构造函数重载时,有可能产生二义性。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,其形参是本类对象的引用。拷贝构造函数的作用是在建立一个新对象时,使用一个已存在的对象去初始化这个新对象。

拷贝构造函数具有以下特点:

  • 因为拷贝构造函数也是一种构造函数,所以其函数名与类名相同,并且该函数也没有返回值。
  • 拷贝构造函数只有一个参数,并且是同类对象的引用。
  • 每个类都必须有一个拷贝构造函数。可以自己定义拷贝构造函数,用于按照需要初始化新对象;如果没有定义类的拷贝构造函数,系统就会自动生成一个默认拷贝构造函数,用于复制出与数据成员值完全相同的新对象。

自定义拷贝构造函数

类名::类名(const 类名 &对象名) 
{
    拷贝构造函数的函数体;
}

class Score{
public:
	Score(int m, int f);  //构造函数
	Score();
	Score(const Score &p);  //拷贝构造函数
	~Score();               //析构函数
	void setScore(int m, int f);
	void showScore();
private:
	int mid_exam;
	int fin_exam;
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
}

Score::Score(const Score &p)
{
	mid_exam = p.mid_exam;
	fin_exam = p.fin_exam;
}

调用拷贝构造函数的一般形式为:
    类名 对象2(对象1);
    类名 对象2 = 对象1;
Score sc1(98, 87);
Score sc2(sc1);    //调用拷贝构造函数
Score sc3 = sc2;   //调用拷贝构造函数

调用拷贝构造函数的三种情况

  • 当用类的一个对象去初始化该类的另一个对象时;
  • 当函数的形参是类的对象,调用函数进行形参和实参结合时;
  • 当函数的返回值是对象,函数执行完成返回调用者时。

浅拷贝和深拷贝

  • 浅拷贝

浅拷贝是指将一个对象的值复制到另一个对象,包括对象的成员变量。对于指针类型的成员变量,浅拷贝只是简单地将指针的值复制过去,这样两个对象将指向同一个内存地址。这意味着,如果其中一个对象修改了指针指向的内存内容,另一个对象也会受到影响。这可能导致悬空指针、内存泄漏和非预期的行为。

class Student{
public:
    Student(char *name1, float score1);
    ~Student();
private:
    char *name;
    float score;
};

如下语句会产生错误
Student stu1("白", 89);
Student stu2(stu1);

上述错误是因为stu1和stu2所指的内存空间相同,在析构函数释放stu1所指的内存后,再释放stu2所指的内存会发生错误,因为此内存空间已被释放。解决方法就是重定义拷贝构造函数,为其变量重新生成内存空间。

Student::Student(const Student& stu)
{
    name = new char[strlen(stu.name) + 1];
    if (name != 0) {
        strcpy(name, stu.name);
        score = stu.score;
    }
}
  • 深拷贝

深拷贝是指创建一个新的对象,并将原始对象的值复制到新对象中,包括对象的成员变量。对于指针类型的成员变量,深拷贝会为新对象分配独立的内存空间,并将原始对象指针指向的内容复制到新的内存空间中。这样,每个对象都有自己独立的资源,修改一个对象不会影响其他对象。

在 C++ 中,可以通过自定义拷贝构造函数和赋值运算符重载函数来实现深拷贝。在这些函数中,需要为每个指针类型的成员变量执行动态内存分配,并将原始对象指针指向的内容复制到新的内存空间中。

以下是一个示例,展示了如何实现深拷贝:

#include 

class MyClass {
private:
    int* data;

public:
    // 构造函数
    MyClass(int value) {
        data = new int(value);
    }

    // 拷贝构造函数(深拷贝)
    MyClass(const MyClass& other) {
        data = new int(*other.data);  // 为新对象分配独立的内存空间,并复制原始对象的值
    }

    // 赋值运算符重载(深拷贝)
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {  // 避免自我赋值
            delete data;  // 释放旧的内存空间
            data = new int(*other.data);  // 为当前对象分配新的内存空间,并复制原始对象的值
        }
        return *this;
    }

    // 打印数据
    void printData() {
        std::cout << "Data: " << *data << std::endl;
    }

    // 析构函数
    ~MyClass() {
        delete data;
    }
};

int main() {
    MyClass obj1(10);  // 创建对象 obj1
    MyClass obj2(obj1);  // 深拷贝,创建对象 obj2 并复制 obj1 的值

    obj1.printData();  // 输出:Data: 10
    obj2.printData();  // 输出:Data: 10

    MyClass obj3(20);  // 创建对象 obj3
    obj2 = obj3;  // 深拷贝,将 obj3 的值赋给 obj2

    obj2.printData();  // 输出:Data: 20

    return 0;
}

在上述示例中,MyClass 类定义了一个深拷贝的拷贝构造函数和赋值运算符重载函数。在这些函数中,我们使用 new 运算符为新对象分配独立的内存空间,并将原始对象的值复制到新的内存空间中。

通过深拷贝,每个对象都有自己独立的 data 内存空间,修改一个对象不会影响其他对象。这种方式可以确保对象的拷贝行为符合预期,并避免因为指针成员变量的浅拷贝而导致的问题。

四.类和对象(二)

4.1 自引用指针this

this指针保存当前对象的地址,称为自引用指针

void Sample::copy(Sample& xy)
{
    if (this == &xy) return;
    *this = xy;
}

this 指针的主要作用是在成员函数中访问当前对象的成员变量和成员函数。通过 this 指针,可以明确地引用当前对象的成员,避免与其他对象的成员发生混淆。

#include 

class MyClass {
private:
    int value;

public:
    // 构造函数
    MyClass(int value) {
        this->value = value;  // 使用 this 指针访问成员变量
    }

    // 成员函数
    void printValue() {
        std::cout << "Value: " << this->value << std::endl;  // 使用 this 指针访问成员变量
    }
};

int main() {
    MyClass obj1(10);
    obj1.printValue();  // 输出:Value: 10

    return 0;
}

4.2 对象数组与对象指针

对象数组

类名 数组名[下标表达式]
用只有一个参数的构造函数给对象数组赋值
Exam ob[4] = {89, 97, 79, 88};
用不带参数和带一个参数的构造函数给对象数组赋值
Exam ob[4] = {89, 90};
用带有多个参数的构造函数给对象数组赋值
Score rec[3] = {Score(33, 99), Score(87, 78), Score(99, 100)};

对象数组

每一个对象在初始化后都会在内存中占有一定的空间。因此,既可以通过对象名访问对象,也可以通过对象地址来访问对象。对象指针就是用于存放对象地址的变量。声明对象指针的一半语法形式为:类名 *对象指针名

Score score;
Score *p;
p = &score;
p->成员函数();

用对象指针访问对象数组

Score score[2];
score[0].setScore(90, 99);
score[1].setScore(67, 89);

Score *p;
p = score;   //将对象score的地址赋值给p
p->showScore();
p++;    //对象指针变量加1
p->showSccore();

Score *q;
q =&score[1];   //将第二个数组元素的地址赋值给对象指针变量q

4.3 string类

C++支持两种类型的字符串,第一种是C语言中介绍过的、包括一个结束符’\0’(即以NULL结束)的字符数组,标准库函数提供了一组对其进行操作的函数,可以完成许多常用的字符串操作。

C++标准库中声明了一种更方便的字符串类型,即字符串类string,类string提供了对字符串进行处理所需要的操作。使用string类必须在程序的开始包括头文件string,即要有以下语句:#include
常用的string类运算符如下:

=、+、+=、==、!=、<、<=、>、>=、[](访问下标对应字符)、>>(输入)、<<(输出)
#include 
#include 
using namespace std;

int main()
{
	string str1 = "ABC";
	string str2("dfdf");
	string str3 = str1 + str2;
	cout<< "str1 = " << str1 << "  str2 = " << str2 << "  str3 = " << str3 << endl;
	str2 += str2;
	str3 += "aff";
	cout << "str2 = " << str2 << "  str3 = " << str3 << endl;
	cout << "str1[1] = " << str1[1] << "  str1 == str2 ? " << (str1 == str2) << endl;
	string str = "ABC";
	cout << "str == str1 ? " << (str == str1) << endl;
	return 0;
}

4.4 向函数传递对象

  • 使用对象作为函数参数:对象可以作为参数传递给函数,其方法与传递其他类型的数据相同。在向函数传递对象时,是通过“传值调用”的方法传递给函数的。因此,函数中对对象的任何修改均不影响调用该函数的对象(实参本身)。
  • 使用对象指针作为函数参数:对象指针可以作为函数的参数,使用对象指针作为函数参数可以实现传值调用,即在函数调用时使实参对象和形参对象指针变量指向同一内存地址,在函数调用过程中,形参对象指针所指的对象值的改变也同样影响着实参对象的值。
  • 使用对象引用作为函数参数:在实际中,使用对象引用作为函数参数非常普遍,大部分程序员喜欢使用对象引用替代对象指针作为函数参数。因为使用对象引用作为函数参数不但具有用对象指针做函数参数的优点,而且用对象引用作函数参数将更简单、更直接。
#include 
using namespace std;

class Point{
public:
	int x;
	int y;
	Point(int x1, int y1) : x(x1), y(y1)  //成员初始化列表
    { }
	int getDistance() 
	{
		return x * x + y * y;
	}
};

void changePoint1(Point point)    //使用对象作为函数参数
{
	point.x += 1;
	point.y -= 1;
}

void changePoint2(Point *point)   //使用对象指针作为函数参数
{
	point->x += 1;
	point->y -= 1;
}

void changePoint3(Point &point)  //使用对象引用作为函数参数
{
	point.x += 1;
	point.y -= 1;
}


int main()
{
	Point point[3] = {Point(1, 1), Point(2, 2), Point(3, 3)};
	Point *p = point;
	changePoint1(*p);
	cout << "the distance is " << p[0].getDistance() << endl;
	p++;
	changePoint2(p);
	cout << "the distance is " << p->getDistance() << endl;
	changePoint3(point[2]);
	cout << "the distance is " << point[2].getDistance() << endl;

	return 0;
}

4.5 静态成员

静态数据成员

在一个类中,若将一个数据成员说明为static,则这种成员被称为静态数据成员。与一般的数据成员不同,无论建立多少个类的对象,都只有一个静态数据成员的拷贝。从而实现了同一个类的不同对象之间的数据共享。

定义静态数据成员的格式如下:static 数据类型 数据成员名;

说明:

  • 静态数据成员的定义与普通数据成员相似,但前面要加上static关键字。

  • 静态数据成员的初始化与普通数据成员不同。静态数据成员初始化应在类外单独进行,而且应在定义对象之前进行。一般在main()函数之前、类声明之后的特殊地带为它提供定义和初始化。

  • 静态数据成员属于类(准确地说,是属于类中对象的集合),而不像普通数据成员那样属于某一对象,因此,可以使用“类名::”访问静态的数据成员。格式如下:类名::静态数据成员名。

  • 静态数据成员与静态变量一样,是在编译时创建并初始化。它在该类的任何对象被建立之前就存在。因此,共有的静态数据成员可以在对象定义之前被访问。对象定以后,共有的静态数据成员也可以通过对象进行访问。其访问格式如下

    对象名.静态数据成员名;
    对象指针->静态数据成员名;
    
#include 

class MyClass {
public:
    static int count;  // 静态数据成员的声明

    MyClass() {
        count++;  // 在构造函数中对静态数据成员进行操作
    }
};

int MyClass::count = 0;  // 静态数据成员的定义和初始化

int main() {
    MyClass obj1;
    MyClass obj2;
    MyClass obj3;

    std::cout << "Count: " << MyClass::count << std::endl;  // 通过类名访问静态数据成员

    return 0;
}

MyClass 类声明了一个静态数据成员 count,并在构造函数中对该静态数据成员进行自增操作。在 main 函数中创建了三个 MyClass 类的实例,每次创建实例时,静态数据成员 count 的值都会增加。最后,通过类名和作用域解析运算符 ::,我们可以访问并输出静态数据成员的值。

需要注意的是,静态数据成员在类外部定义和初始化时,不需要再使用 static 关键字。而在类内部声明时,需要使用 static 关键字进行标识。静态数据成员可以在类外部进行初始化,也可以在类内部进行初始化。如果在类内部进行初始化,只能在声明时进行初始化,不能在构造函数或其他函数中进行初始化。

静态数据成员的主要特点是它们被所有类的实例共享,可以用于表示类的全局状态或计数器等共享信息。

静态成员函数

在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。静态成员函数的作用不是为了对象之间的沟通,而是为了处理静态数据成员。定义静态成员函数的格式如下:

static 返回类型 静态成员函数名(参数表);

与静态数据成员类似,调用公有静态成员函数的一般格式有如下几种:

类名::静态成员函数名(实参表);
对象.静态成员函数名(实参表);
对象指针->静态成员函数名(实参表);

一般而言,静态成员函数不访问类中的非静态成员。若确实需要,静态成员函数只能通过对象名(或对象指针、对象引用)访问该对象的非静态成员。

下面对静态成员函数的使用再做几点说明:

  • 一般情况下,静态函数成员主要用来访问静态成员函数。当它与静态数据成员一起使用时,达到了对同一个类中对象之间共享数据的目的。
  • 私有静态成员函数不能被类外部的函数和对象访问。
  • 使用静态成员函数的一个原因是,可以用它在建立任何对象之前调用静态成员函数,以处理静态数据成员,这是普通成员函数不能实现的功能
  • 编译系统将静态成员函数限定为内部连接,也就是说,与现行文件相连接的其他文件中的同名函数不会与该函数发生冲突,维护了该函数使用的安全性,这是使用静态成员函数的另一个原因。
  • 静态成员函数是类的一部分,而不是对象的一部分。如果要在类外调用公有的静态成员函数,使用如下格式较好:类名::静态成员函数名()
#include 
using namespace std;

class Score{
private:
	int mid_exam;
	int fin_exam;
	static int count;     //静态数据成员,用于统计学生人数
	static float sum;     //静态数据成员,用于统计期末累加成绩
	static float ave;     //静态数据成员,用于统计期末平均成绩
public:
	Score(int m, int f);
	~Score();
	static void show_count_sum_ave();   //静态成员函数
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
	++count;
	sum += fin_exam;
	ave = sum / count;
}

Score::~Score()
{

}

/*** 静态成员初始化 ***/
int Score::count = 0;
float Score::sum = 0.0;
float Score::ave = 0.0;

void Score::show_count_sum_ave()
{
	cout << "学生人数: " << count << endl;
	cout << "期末累加成绩: " << sum << endl;
	cout << "期末平均成绩: " << ave << endl;
}

int main()
{
	Score sco[3] = {Score(90, 89), Score(78, 99), Score(89, 88)};
	sco[2].show_count_sum_ave();
	Score::show_count_sum_ave();

	return 0;
}

4.6 友元

类的主要特点之一是数据隐藏和封装,即类的私有成员(或保护成员)只能在类定义的范围内使用,也就是说私有成员只能通过它的成员函数来访问。但是,有时为了访问类的私有成员而需要在程序中多次调用成员函数,这样会因为频繁调用带来较大的时间和空间开销,从而降低程序的运行效率。为此,C++提供了友元来对私有或保护成员进行访问。友元包括友元函数和友元类。
友元函数

友元函数既可以是不属于任何类的非成员函数,也可以是另一个类的成员函数。友元函数不是当前类的成员函数,但它可以访问该类的所有成员,包括私有成员、保护成员和公有成员。

在类中声明友元函数时,需要在其函数名前加上关键字friend。此声明可以放在公有部分,也可以放在保护部分和私有部分。友元函数可以定义在类内部,也可以定义在类外部。

1.将非成员函数声明为友元函数

#include 
using namespace std;
class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f);
	void showScore();
	friend int getScore(Score &ob);
};

Score::Score(int m, int f)
{
	mid_exam = m;
	fin_exam = f;
}

int getScore(Score &ob)
{
	return (int)(0.3 * ob.mid_exam + 0.7 * ob.fin_exam);
}

int main()
{
	Score score(98, 78);
	cout << "成绩为: " << getScore(score) << endl;

	return 0;
}

说明:

  • 友元函数虽然可以访问类对象的私有成员,但他毕竟不是成员函数。因此,在类的外部定义友元函数时,不必像成员函数那样,在函数名前加上类名::
  • 因为友元函数不是类的成员,所以它不能直接访问对象的数据成员,也不能通过this指针访问对象的数据成员,它必须通过作为入口参数传递进来的对象名(或对象指针、对象引用)来访问该对象的数据成员。
  • 友元函数提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。尤其当一个函数需要访问多个类时,友元函数非常有用,普通的成员函数只能访问其所属的类,但是多个类的友元函数能够访问相关的所有类的数据。

2.将成员函数声明为友元函数

一个类的成员函数可以作为另一个类的友元,它是友元函数中的一种,称为友元成员函数。友元成员函数不仅可以访问自己所在类对象中的私有成员和公有成员,还可以访问friend声明语句所在类对象中的所有成员,这样能使两个类相互合作、协调工作,完成某一任务。

#include 
#include 
using namespace std;

class Score;    //对Score类的提前引用说明
class Student{
private:
	string name;
	int number;
public:
	Student(string na, int nu) {
		name = na;
		number = nu;
	}
	void show(Score &sc);
};

class Score{
private:
	int mid_exam;
	int fin_exam;
public:
	Score(int m, int f) {
		mid_exam = m;
		fin_exam = f;
	}
	friend void Student::show(Score &sc);
};

void Student::show(Score &sc) {
	cout << "姓名:" << name << "  学号:" << number << endl;
	cout << "期中成绩:" << sc.mid_exam << "  期末成绩:" << sc.fin_exam << endl;
}

int main() {
	Score sc(89, 99);
	Student st("白", 12467);
	st.show(sc);

	return 0;
}

**说明:**一个类的成员函数作为另一个类的友元函数时,必须先定义这个类。并且在声明友元函数时,需要加上成员函数所在类的类名;

友元类

可以将一个类声明为另一个类的友元

class Y{
    ···
};
class X{
    friend Y;    //声明类Y为类X的友元类
};

当一个类被说明为另一个类的友元类时,它所有的成员函数都成为另一个类的友元函数,这就意味着作为友元类中的所有成员函数都可以访问另一个类中的所有成员。

友元关系不具有交换性传递性

4.7 类的组合

在一个类中内嵌另一个类的对象作为数据成员,称为类的组合。该内嵌对象称为对象成员,又称为子对象

class Y{
    ···
};
class X{
    Y y;
    ···
};

五.继承和派生

继承可以在已有类的基础上创建新的类,新类可以从一个或多个已有类中继承成员函数和数据成员,而且可以重新定义或加进新的数据和函数,从而形成类的层次或等级。其中,已有类称为基类父类,在它基础上建立的新类称为派生类子类

5.1 继承和派生的概念

类的继承是新的类从已有类那里得到已有的特性。从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。类的继承和派生机制较好地解决了代码重用的问题。

关于基类和派生类的关系,可以表述为:派生类是基类的具体化,而基类则是派生类的抽象。

使用继承的案例如下:

#include 
#include 
using namespace std;

class Person{
private:
	string name;
	string id_number;
	int age;
public:
	Person(string name1, string id_number1, int age1) {
		name = name1;
		id_number = id_number1;
		age = age1;
	}
	~Person() {

	}
	void show() {
		cout << "姓名: " << name << "  身份证号: " << id_number << " 年龄: " << age << endl;
	}
};

class Student:public Person{
private:
	int credit;
public:
	Student(string name1, string id_number1, int age1, int credit1):Person(name1, id_number1, credit1) {
		credit = credit1;
	}
	~Student() {

	}
	void show() {
		Person::show();
		cout << "学分: " << credit << endl;
	}
};

int main() {
	Student stu("白", "110103**********23", 12, 123);
	stu.show();

	return 0;
}

从已有类派生出新类时,可以在派生类内完成以下几种功能:

  1. 可以增加新的数据成员和成员函数

  2. 可以对基类的成员进行重定义

  3. 可以改变基类成员在派生类中的访问属性

基类成员在派生类的访问属性

派生类可以继承基类中除了构造函数与析构函数之外的成员,但是这些成员的访问属性在派生过程中是可以调整的。从基类继承来的成员在派生类中的访问属性也有所不同。

派生类对基类的访问规则

基类的成员可以有public、protected、private3中访问属性,基类的成员函数可以访问基类中其他成员,但是在类外通过基类的对象,就只能访问该基类的公有成员。同样,派生类的成员也可以有public、protected、private3种访问属性,派生类的成员函数可以访问派生类中自己增加的成员,但是在派生类外通过派生类的对象,就只能访问该派生类的公有成员。

派生类对基类成员的访问形式主要有以下两种:

  • 内部访问:由派生类中新增的成员函数对基类继承来的成员的访问。
  • 对象访问:在派生类外部,通过派生类的对象对从基类继承来的成员的访问。

5.2 派生类的构造函数与析构函数

构造函数的主要作用是对数据进行初始化。在派生类中,如果对派生类新增的成员进行初始化,就需要加入派生类的构造函数。与此同时,对所有从基类继承下来的成员的初始化工作,还是由基类的构造函数完成,但是基类的构造函数和析构函数不能被继承,因此必须在派生类的构造函数中对基类的构造函数所需要的参数进行设置。同样,对撤销派生类对象的扫尾、清理工作也需要加入新的析构函数来完成。

#include 
#include 
using namespace std;

class A{
public:
	A() {
		cout << "A类对象构造中..." << endl;
	}
	~A() {
		cout << "析构A类对象..." << endl;
	}
};

class B : public A{
public:
	B() {
		cout << "B类对象构造中..." << endl;
	}
	~B(){
		cout << "析构B类对象..." << endl;
	}
};

int main() {
	B b;
	return 0;
}

代码运行结果如下:

A类对象构造中...
B类对象构造中...
析构B类对象...
析构A类对象...

可见:构造函数的调用严格地按照先调用基类的构造函数,后调用派生类的构造函数的顺序执行。析构函数的调用顺序与构造函数的调用顺序正好相反,先调用派生类的析构函数,后调用基类的析构函数。

派生类对基类的访问规则

  • 公有继承:当派生类使用公有继承方式继承基类时,基类的公有成员和保护成员在派生类中保持相应的访问权限。派生类可以直接访问基类的公有成员和保护成员。

  • 保护继承:当派生类使用保护继承方式继承基类时,基类的公有成员和保护成员在派生类中变为保护成员。派生类可以直接访问基类的保护成员,但无法直接访问基类的公有成员。

  • 私有继承:当派生类使用私有继承方式继承基类时,基类的公有成员和保护成员在派生类中变为私有成员。派生类无法直接访问基类的公有成员和保护成员。

#include 

class Base {
public:
    int publicMember;
protected:
    int protectedMember;
private:
    int privateMember;
};

class DerivedPublic : public Base {
public:
    void accessBaseMembers() {
        publicMember = 1;       // 可以访问基类的公有成员
        protectedMember = 2;    // 可以访问基类的保护成员
        // privateMember = 3;   // 无法访问基类的私有成员
    }
};

class DerivedProtected : protected Base {
public:
    void accessBaseMembers() {
        // publicMember = 1;    // 无法访问基类的公有成员
        protectedMember = 2;    // 可以访问基类的保护成员
        // privateMember = 3;   // 无法访问基类的私有成员
    }
};

class DerivedPrivate : private Base {
public:
    void accessBaseMembers() {
        // publicMember = 1;    // 无法访问基类的公有成员
        // protectedMember = 2; // 无法访问基类的保护成员
        // privateMember = 3;   // 无法访问基类的私有成员
    }
};

int main() {
    DerivedPublic derivedPublic;
    derivedPublic.accessBaseMembers();

    DerivedProtected derivedProtected;
    derivedProtected.accessBaseMembers();

    DerivedPrivate derivedPrivate;
    derivedPrivate.accessBaseMembers();

    return 0;
}

需要注意的是,不论继承方式是公有、保护还是私有,派生类都无法直接访问基类的私有成员。私有成员只能在基类内部访问。

派生类构造函数的一般格式为:
派生类名(参数总表):基类名(参数表) {
    派生类新增数据成员的初始化语句
}
-----------------------------------------------------------------
含有子对象的派生类的构造函数:
派生类名(参数总表):基类名(参数表0),子对象名1(参数表1),...,子对象名n(参数表n)
{
    派生类新增成员的初始化语句
}

在定义派生类对象时,构造函数的调用顺序如下:

  1. 调用基类的构造函数,对基类数据成员初始化。

  2. 调用子对象的构造函数,对子对象的数据成员初始化。

  3. 调用派生类的构造函数体,对派生类的数据成员初始化。

说明:

  • 当基类构造函数不带参数时,派生类不一定需要定义构造函数;然而当基类的构造函数哪怕只带有一个参数,它所有的派生类都必须定义构造函数,甚至所定义的派生类构造函数的函数体可能为空,它仅仅起参数的传递作用。

  • 若基类使用默认构造函数或不带参数的构造函数,则在派生类中定义构造函数时可略去“:基类构造函数名(参数表)”,此时若派生类也不需要构造函数,则可不定义构造函数。

  • 如果派生类的基类也是一个派生类,每个派生类只需负责其直接基类数据成员的初始化,依次上溯。

5.3 多继承

声明多继承派生类的一般形式如下:

class 派生类名:继承方式1 基类名1,...,继承方式n 基类名n {
    派生类新增的数据成员和成员函数
};
默认的继承方式是private

多继承派生类的构造函数与析构函数:

与单继承派生类构造函数相同,多重继承派生类构造函数必须同时负责该派生类所有基类构造函数的调用。

多继承构造函数的调用顺序与单继承构造函数的调用顺序相同,也是遵循先调用基类的构造函数,再调用对象成员的构造函数,最后调用派生类构造函数的原则。析构函数的调用与之相反。

六.多态性与虚函数

多态性是面向对象程序设计的重要特征之一。多态性机制不仅增加了面向对象软件系统的灵活性,进一步减少了冗余信息,而且显著提高了软件的可重用性和可扩充性。多态性的应用可以使编程显得更简洁便利,它为程序的模块化设计又提供了一种手段。

6.1 多态性概述

C++中的多态性(Polymorphism)是面向对象编程的一个重要特性,它允许在不同的对象上使用相同的接口来实现不同的行为。多态性通过继承和虚函数实现,可以提高代码的灵活性和可扩展性。

在C++中,多态性主要有两种形式:静态多态性(静态多态性)和动态多态性(动态多态性)。

  1. 静态多态性(静态多态性):

    • 静态多态性是通过函数重载和运算符重载实现的,它在编译时确定调用的函数或运算符的版本。
    • 函数重载(Function Overloading)允许在同一个作用域内定义多个同名函数,但它们的参数类型、参数个数或参数顺序不同。编译器根据函数调用时提供的参数类型和个数来确定调用哪个函数。
    • 运算符重载(Operator Overloading)允许重新定义已有的运算符的行为。通过重载运算符,可以使得对象之间的操作更加自然和直观。
  2. 动态多态性(动态多态性):

    • 动态多态性是通过继承和虚函数实现的,它在运行时确定调用的函数版本。
    • 继承(Inheritance)允许创建一个新的类(子类或派生类),它继承了已有类(父类或基类)的属性和行为。子类可以重写父类的成员函数,并通过父类的指针或引用调用这些函数。
    • 虚函数(Virtual Function)是在基类中声明的函数,它可以在派生类中被重写。通过在基类中将函数声明为虚函数,可以实现运行时的动态绑定,即在运行时根据对象的实际类型确定调用的函数版本。

动态多态性对于实现运行时的多态行为非常重要,它允许以统一的方式处理不同类型的对象,提高代码的可维护性和可扩展性。通过使用基类的指针或引用,可以实现多态的调用,即使在编译时无法确定对象的具体类型,也可以根据对象的实际类型调用相应的函数。

以下是一个简单的示例,展示了静态多态性和动态多态性的使用:

#include 

class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape." << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

int main() {
    Shape shape;
    Circle circle;

    shape.draw();   // 静态多态性,调用 Shape 类的 draw 函数
    circle.draw();  // 静态多态性,调用 Circle 类的 draw 函数

    Shape* shapePtr = &circle;
    shapePtr->draw();  // 动态多态性,根据对象的实际类型调用相应的 draw 函数

    return 0;
}

在上述示例中,Shape类是一个基类,Circle类是其派生类。通过在基类中将draw函数声明为虚函数,并在派生类中重写该函数,实现了动态多态性。通过基类的指针shapePtr指向派生类的对象circle,可以在运行时根据对象的实际类型调用相应的draw函数。

6.2 虚函数

虚函数的定义是在基类中进行的,它是在基类中需要定义为虚函数的成员函数的声明中冠以关键字virtual,从而提供一种接口界面。定义虚函数的方法如下:

virtual 返回类型 函数名(形参表) {
    函数体
}

在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或多个派生类中被重新定义。虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型的顺序,都必须与基类中的原型完全相同。

#include 
#include 
using namespace std;

class Family{
private:
	string flower;
public:
	Family(string name = "鲜花"): flower(name) { }
	string getName() {
		return flower;
	}
	virtual void like() {
		cout << "家人喜欢不同的花: " << endl;
	}
};

class Mother: public Family{
public:
	Mother(string name = "月季"): Family(name) { }
	void like() {
		cout << "妈妈喜欢" << getName() << endl;
	}
};

class Daughter: public Family{
public:
	Daughter(string name = "百合"): Family(name) { }
	void like() {
		cout << "女儿喜欢" << getName() << endl;
	}
};

int main() {
	Family *p;
	Family f;
	Mother mom;
	Daughter dau;
	p = &f;
	p->like();
	p = &mom;
	p->like();
	p = &dau;
	p->like();

	return 0;

程序运行结果如下:

家人喜欢不同的花:
妈妈喜欢月季
女儿喜欢百合

C++规定,如果在派生类中,没有用virtual显式地给出虚函数声明,这时系统就会遵循以下的规则来判断一个成员函数是不是虚函数:该函数与基类的虚函数是否有相同的名称、参数个数以及对应的参数类型、返回类型或者满足赋值兼容的指针、引用型的返回类型。

下面对虚函数的定义做几点说明:

  • 由于虚函数使用的基础是赋值兼容规则,而赋值兼容规则成立的前提条件是派生类从其基类公有派生。因此,通过定义虚函数来使用多态性机制时,派生类必须从它的基类公有派生。
  • 必须首先在基类中定义虚函数;
  • 在派生类对基类中声明的虚函数进行重新定义时,关键字virtual可以写也可以不写。
  • 虽然使用对象名和点运算符的方式也可以调用虚函数,如mom.like()可以调用虚函数Mother::like()。但是,这种调用是在编译时进行的静态连编,它没有充分利用虚函数的特性,只有通过基类指针访问虚函数时才能获得运行时的多态性
  • 一个虚函数无论被公有继承多少次,它仍然保持其虚函数的特性。
  • 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数,因为虚函数调用要靠特定的对象来决定该激活哪个函数。
  • 内联函数不能是虚函数,因为内联函数是不能在运行中动态确定其位置的。即使虚函数在类的内部定义,编译时仍将其看做非内联的。
  • 构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常说明为虚函数。

6.3 虚析构函数

如果在主函数中用new运算符建立一个派生类的无名对象和定义一个基类的对象指针,并将无名对象的地址赋值给这个对象指针,当用delete运算符撤销无名对象时,系统只执行基类的析构函数,而不执行派生类的析构函数。

Base *p;
p = new Derived;
delete p;
-----------------
输出:调用基类Base的析构函数

原因是当撤销指针p所指的派生类的无名对象,而调用析构函数时,采用了静态连编方式,只调用了基类Base的析构函数。

如果希望程序执行动态连编方式,在用delete运算符撤销派生类的无名对象时,先调用派生类的析构函数,再调用基类的析构函数,可以将基类的析构函数声明为虚析构函数。一般格式为

virtual ~类名(){
    ·····
}

虽然派生类的析构函数与基类的析构函数名字不相同,但是如果将基类的析构函数定义为虚函数,由该类所派生的所有派生类的析构函数也都自动成为虚函数。示例如下:

#include 
#include 
using namespace std;

class Base{
public:
	virtual ~Base() {
		cout << "调用基类Base的析构函数..." << endl;
	}
};

class Derived: public Base{
public:
	~Derived() {
		cout << "调用派生类Derived的析构函数..." << endl;
	}
};

int main() {
	Base *p;
	p = new Derived;
	delete p;
	return 0;
}

输出如下:

调用派生类Derived的析构函数...
调用基类Base的析构函数...

6.4 纯虚函数

纯虚函数(Pure Virtual Function)是在基类中声明但没有实现的虚函数。它的声明形式为在函数声明的结尾加上= 0,表示该函数是纯虚函数。纯虚函数在基类中没有具体的实现,而是由派生类来实现。

纯虚函数的主要作用是为了定义一个接口,要求派生类必须实现这个函数。它可以充当抽象类(Abstract Class)的一部分,抽象类是不能被实例化的类,只能作为基类来派生其他类。

派生类必须实现基类中的纯虚函数,否则派生类也会成为抽象类。纯虚函数的存在使得基类可以定义一组接口规范,而将具体的实现留给派生类来完成。通过这种方式,可以实现多态性,通过基类的指针或引用调用派生类的函数。

以下是一个简单的示例,展示了纯虚函数的使用:

#include 

class Animal {
public:
    virtual void makeSound() const = 0;  // 纯虚函数

    void eat() {
        std::cout << "Animal is eating." << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() const override {
        std::cout << "Dog barks." << std::endl;
    }
};

class Cat : public Animal {
public:
    void makeSound() const override {
        std::cout << "Cat meows." << std::endl;
    }
};

int main() {
    Dog dog;
    Cat cat;

    dog.makeSound();  // 调用 Dog 类的 makeSound 函数
    cat.makeSound();  // 调用 Cat 类的 makeSound 函数

    Animal* animalPtr = &dog;
    animalPtr->makeSound();  // 动态多态性,根据对象的实际类型调用相应的 makeSound 函数

    return 0;
}

在上述示例中,Animal类是一个抽象类,其中的makeSound函数是纯虚函数。DogCat类是Animal类的派生类,并实现了makeSound函数。通过基类的指针animalPtr指向派生类的对象,可以在运行时根据对象的实际类型调用相应的makeSound函数。

6.5 抽象类

抽象类(Abstract Class)是一个不能被实例化的类,它只能作为基类来派生其他类。一个类至少有一个纯虚函数,那么就称该类为抽象类

抽象类的主要作用是定义一组接口规范,而将具体的实现留给派生类来完成。通过抽象类,可以实现多态性,通过基类的指针或引用调用派生类的函数。

抽象类具有以下特点:

  • 抽象类不能被实例化,即不能创建抽象类的对象。
  • 抽象类可以包含成员变量、成员函数(包括纯虚函数)和构造函数/析构函数。
  • 抽象类中的纯虚函数没有具体的实现,只有函数的声明,派生类必须实现这些纯虚函数。
  • 如果一个类从抽象类派生而来,但没有实现基类中的所有纯虚函数,那么该派生类也会成为抽象类。

以下是一个简单的示例,展示了抽象类的使用:

#include 

class Shape {
public:
    virtual void draw() const = 0;  // 纯虚函数

    void printArea() const {
        std::cout << "Area: " << getArea() << std::endl;
    }

    virtual double getArea() const {
        return 0.0;
    }
};

class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    void draw() const override {
        std::cout << "Drawing a circle." << std::endl;
    }

    double getArea() const override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    void draw() const override {
        std::cout << "Drawing a rectangle." << std::endl;
    }

    double getArea() const override {
        return width * height;
    }
};

int main() {
    Circle circle(5.0);
    Rectangle rectangle(3.0, 4.0);

    circle.draw();      // 调用 Circle 类的 draw 函数
    circle.printArea(); // 调用 Circle 类的 printArea 函数

    rectangle.draw();      // 调用 Rectangle 类的 draw 函数
    rectangle.printArea(); // 调用 Rectangle 类的 printArea 函数

    Shape* shapePtr = &circle;
    shapePtr->draw();      // 动态多态性,根据对象的实际类型调用相应的 draw 函数
    shapePtr->printArea(); // 调用 Shape 类的 printArea 函数

    return 0;
}

在上述示例中,Shape类是一个抽象类,其中的draw函数是纯虚函数。CircleRectangle类是Shape类的派生类,并实现了draw函数和getArea函数。通过基类的指针shapePtr指向派生类的对象,可以在运行时根据对象的实际类型调用相应的函数。

说明:

  • 由于抽象类中至少包含一个没有定义功能的纯虚函数。因此,抽象类只能作为其他类的基类来使用,不能建立抽象类对象。

  • 不允许从具体类派生出抽象类。所谓具体类,就是不包含纯虚函数的普通类。

  • 抽象类不能用作函数的参数类型、函数的返回类型或是显式转换的类型。

  • 可以声明指向抽象类的指针或引用,此指针可以指向它的派生类,进而实现多态性。

  • 如果派生类中没有定义纯虚函数的实现,而派生类中只是继承基类的纯虚函数,则这个派生类仍然是一个抽象类。如果派生类中

    给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。

七.运算符重载

运算符重载(Operator Overloading)是一种特殊的函数重载,允许程序员重新定义已有的运算符的行为。通过运算符重载,可以使得自定义的类类型对象可以像内置类型一样使用运算符进行操作。

C++中可以重载的运算符包括算术运算符(如+-*/等)、关系运算符(如==!=<>等)、逻辑运算符(如&&||!等)、赋值运算符(如=+=-=等)等。

函数类型  operator  运算符名称(形参表列)
{
	对运算符的重载处理
}

运算符重载函数是类的成员函数或非成员函数,函数名以operator关键字开头,后面紧跟要重载的运算符符号。运算符重载函数可以定义为成员函数时,它们可以访问类的私有成员;定义为非成员函数时,它们不能直接访问类的私有成员,但可以通过友元关系或公有接口间接访问私有成员。

以下是一个简单的示例,展示了运算符重载的使用:

#include 

class Complex {
private:
    double real;
    double imaginary;

public:
    Complex(double r = 0.0, double i = 0.0) : real(r), imaginary(i) {}

    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imaginary + other.imaginary);
    }

    Complex operator-(const Complex& other) const {
        return Complex(real - other.real, imaginary - other.imaginary);
    }

    Complex operator*(const Complex& other) const {
        double r = real * other.real - imaginary * other.imaginary;
        double i = real * other.imaginary + imaginary * other.real;
        return Complex(r, i);
    }

    bool operator==(const Complex& other) const {
        return (real == other.real) && (imaginary == other.imaginary);
    }

    friend std::ostream& operator<<(std::ostream& os, const Complex& complex);
};

std::ostream& operator<<(std::ostream& os, const Complex& complex) {
    os << complex.real << " + " << complex.imaginary << "i";
    return os;
}

int main() {
    Complex c1(2.0, 3.0);
    Complex c2(1.0, 4.0);

    Complex sum = c1 + c2;
    Complex diff = c1 - c2;
    Complex product = c1 * c2;

    std::cout << "c1: " << c1 << std::endl;
    std::cout << "c2: " << c2 << std::endl;
    std::cout << "Sum: " << sum << std::endl;
    std::cout << "Difference: " << diff << std::endl;
    std::cout << "Product: " << product << std::endl;

    if (c1 == c2) {
        std::cout << "c1 and c2 are equal." << std::endl;
    } else {
        std::cout << "c1 and c2 are not equal." << std::endl;
    }

    return 0;
}

八.函数模板与类模板

8.1 函数模板

函数模板(Function Template)是一种通用的函数定义,可以用于生成特定类型的函数。函数模板允许在不同的数据类型上重用相同的代码,提高了代码的可重用性和灵活性。

函数的声明格式如下

template 
返回类型 函数名(模板形参表)
{
    函数体
}
也可以定义为如下形式
template 
返回类型 函数名(模板形参表)
{
    函数体
}

函数模板的定义使用关键字template,后面跟上模板参数列表,其中可以包含一个或多个类型参数或非类型参数。类型参数用typenameclass关键字声明,非类型参数可以是整型、浮点型、指针等。

以下是一个简单的函数模板示例,展示了如何编写和使用函数模板:

#include 

// 函数模板,用于计算两个数的和
template <typename T>
T sum(T a, T b) {
    return a + b;
}

int main() {
    int a = 5, b = 3;
    double x = 2.5, y = 1.8;

    std::cout << "Sum of integers: " << sum(a, b) << std::endl;
    std::cout << "Sum of doubles: " << sum(x, y) << std::endl;

    return 0;
}

在上述示例中,sum是一个函数模板,用于计算两个数的和。模板参数T表示类型参数,可以是任意类型。在main函数中,我们分别使用sum函数模板计算了两个整数和两个浮点数的和,并将结果输出到标准输出流中。

运行示例代码会输出以下结果:

Sum of integers: 8
Sum of doubles: 4.3

可以看到,通过函数模板,我们可以在不同的数据类型上重用相同的代码,实现了对不同类型数据的通用操作。

除了类型参数,函数模板还可以使用非类型参数。非类型参数可以是整型、浮点型、指针等,它们在模板参数列表中以常量表达式的形式出现。使用非类型参数的函数模板可以在编译时根据参数的值进行特化,生成不同的函数定义。以下是一个使用非类型参数的函数模板示例:

#include 

// 函数模板,用于计算数组的平均值
template <typename T, int N>
T average(T (&arr)[N]) {
    T sum = 0;
    for (int i = 0; i < N; i++) {
        sum += arr[i];
    }
    return sum / N;
}

int main() {
    int arr1[] = {1, 2, 3, 4, 5};
    double arr2[] = {1.5, 2.5, 3.5, 4.5, 5.5};

    std::cout << "Average of integers: " << average(arr1) << std::endl;
    std::cout << "Average of doubles: " << average(arr2) << std::endl;

    return 0;
}

在上述示例中,average是一个函数模板,用于计算数组的平均值。模板参数T表示类型参数,N表示非类型参数,表示数组的大小。在main函数中,我们分别使用average函数模板计算了一个整型数组和一个浮点型数组的平均值,并将结果输出到标准输出流中。

运行示例代码会输出以下结果:

Average of integers: 3
Average of doubles: 3.5

可以看到,通过使用非类型参数,函数模板可以根据参数的值生成不同的函数定义,实现了对不同大小数组的通用操作。

8.2 类模板

类模板(Class Template)是一种通用的类定义,可以用于生成特定类型的类。类模板允许在不同的数据类型上重用相同的类结构和成员函数,提高了代码的可重用性和灵活性。

类模板的定义使用关键字template,后面跟上模板参数列表,其中可以包含一个或多个类型参数或非类型参数。类型参数用typenameclass关键字声明,非类型参数可以是整型、浮点型、指针等。

以下是一个简单的类模板示例,展示了如何编写和使用类模板:

#include 

// 类模板,表示二维向量
template <typename T>
class Vector2D {
private:
    T x;
    T y;

public:
    Vector2D(T x = 0, T y = 0) : x(x), y(y) {}

    T getX() const {
        return x;
    }

    T getY() const {
        return y;
    }

    void setX(T newX) {
        x = newX;
    }

    void setY(T newY) {
        y = newY;
    }

    void print() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Vector2D<int> v1(2, 3);
    Vector2D<double> v2(1.5, 2.5);

    v1.print();
    v2.print();

    return 0;
}

在上述示例中,Vector2D是一个类模板,表示二维向量。模板参数T表示类型参数,可以是任意类型。Vector2D类模板包含了私有数据成员xy,以及公有成员函数用于获取和设置数据成员的值,并打印向量的坐标。

main函数中,我们分别实例化了一个整型向量v1和一个浮点型向量v2,并调用了它们的成员函数print打印向量的坐标。

运行示例代码会输出以下结果:

(2, 3)
(1.5, 2.5)

可以看到,通过类模板,我们可以在不同的数据类型上重用相同的类结构和成员函数,实现了对不同类型向量的通用操作。

类模板还可以使用多个类型参数,以及非类型参数。多个类型参数可以同时指定不同的数据类型,非类型参数可以在编译时根据参数的值进行特化,生成不同的类定义。以下是一个使用多个类型参数和非类型参数的类模板示例:

#include 

// 类模板,表示二维矩阵
template <typename T, int Rows, int Cols>
class Matrix {
private:
    T data[Rows][Cols];

public:
    Matrix() {
        for (int i = 0; i < Rows; i++) {
            for (int j = 0; j < Cols; j++) {
                data[i][j] = T();
            }
        }
    }

    void print() const {
        for (int i = 0; i < Rows; i++) {
            for (int j = 0; j < Cols; j++) {
                std::cout << data[i][j] << " ";
            }
            std::cout << std::endl;
        }
    }
};

int main() {
    Matrix<int, 2, 3> m1;
    Matrix<double, 3, 2> m2;

    m1.print();
    std::cout << std::endl;
    m2.print();

    return 0;
}

在上述示例中,Matrix是一个类模板,表示二维矩阵。模板参数T表示类型参数,RowsCols表示非类型参数,分别表示矩阵的行数和列数。Matrix类模板包含了私有数据成员data,以及公有成员函数用于打印矩阵的元素。

main函数中,我们分别实例化了一个整型矩阵m1(2行3列)和一个浮点型矩阵m2(3行2列),并调用了它们的成员函数print打印矩阵的元素。

运行示例代码会输出以下结果:

0 0 0
0 0 0

0 0 
0 0 
0 0 

可以看到,通过使用多个类型参数和非类型参数,类模板可以同时指定不同的数据类型和不同大小的矩阵,实现了对不同类型和大小矩阵的通用操作。

九.C++的输入与输出

9.1 C++为何建立自己的输入/输出系统

C++除了完全支持C语言的输入输出系统外,还定义了一套面向对象的输入/输出系统。C++的输入输出系统比C语言更安全、可靠。

c++的输入/输出系统明显地优于C语言的输入/输出系统。首先,它是类型安全的、可以防止格式控制符与输入输出数据的类型不一致的错误。另外,C++可以通过重载运算符“>>”和"<<",使之能用于用户自定义类型的输入和输出,并且向预定义类型一样有效方便。C++的输入/输出的书写形式也很简单、清晰,这使程序代码具有更好的可读性。

9.2 C++的流库及其输出结构

“流”指的是数据从一个源流到一个目的的抽象,它负责在数据的生产者(源)和数据的消费者(目的)之间建立联系,并管理数据的流动。凡是数据从一个地方传输到另一个地方的操作都是流的操作,从流中提取数据称为输入操作(通常又称提取操作),向流中添加数据称为输出操作(通常又称插入操作)。

C++的输入/输出是以字节流的形式实现的。在输入操作中,字节流从输入设备(如键盘、磁盘、网络连接等)流向内存;在输出操作中,字节流从内存流向输出设备(如显示器、打印机、网络连接等)。字节流可以是ASCII码、二进制形式的数据、图形/图像、音频/视频等信息。文件和字符串也可以看成有序的字节流,分别称为文件流和字符串流。
用于输入/输出的头文件

C++编译系统提供了用于输入/输出的I/O类流库。I/O流类库提供了数百种输入/输出功能,I/O流类库中各种类的声明被放在相应的头文件中,用户在程序中用#include命令包含了有关的头文件就相当于在本程序中声明了所需要用到的类。常用的头文件有:

  • iostream包含了对输入/输出流进行操作所需的基本信息。使用cin、cout等流对象进行针对标准设备的I/O操作时,须包含此头文件。
  • fstream用于用户管理文件的I/O操作。使用文件流对象进行针对磁盘文件的操作,须包含此头文件。
  • strstream用于字符串流的I/O操作。使用字符串流对象进行针对内存字符串空间的I/O操作,须包含此头文件。
  • iomanip用于输入/输出的格式控制。在使用setw、fixed等大多数操作符进行格式控制时,须包含此头文件。

用于输入/输出的流类

I/O流类库中包含了许多用于输入/输出操作的类。其中,类istream支持流输入操作,类ostream支持流输出操作,类iostream同时支持流输入和输出操作。

下表列出了iostream流类库中常用的流类,以及指出了这些流类在哪个头文件中声明。

类名 类名 说明 头文件
抽象流基类 ios 流基类 iostream
输入流类 istream 通用输入流类和其他输入流的基类 iostream
输入流类 ifstream 输入文件流类 fstream
输入流类 istrstream 输入字符串流类 strstream
输出流类 ostream 通用输出流类和其他输出流的基类 iostream
输出流类 ofstream 输出文件流类 fstream
输出流类 ostrstream 输出字符串流类 strstream
输入/输出流类 iostream 通用输入输出流类和其他输入/输出流的基类 iostream
输入/输出流类 fstream 输入/输出文件流类 fstream
输入/输出流类 strstream 输入/输出字符串流类 strstream

9.3 预定的流对象

用流定义的对象称为流对象。与输入设备(如键盘)相关联的流对象称为输入流对象;与输出设备(如屏幕)相联系的流对象称为输出流对象。

C++中包含几个预定义的流对象,它们是标准输入流对象cin、标准输出流对象cout、非缓冲型的标准出错流对象cerr和缓冲型的标准出错流对象clog

9.4 输入/输出流的成员函数

使用istream和类ostream流对象的一些成员函数,实现字符的输出和输入。

1、put()函数
    cout.put(单字符/字符形变量/ASCII码);
2、get()函数
    get()函数在读入数据时可包括空白符,而提取运算符“>>”在默认情况下拒绝接收空白字符。
    cin.get(字符型变量)
3、getline()函数
    cin.getline(字符数组, 字符个数n, 终止标志字符)
    cin.getline(字符指针, 字符个数n, 终止标志字符)
4、ignore()函数
    cin.ignore(n, 终止字符)
    ignore()函数的功能是跳过输入流中n个字符(默认个数为1),或在遇到指定的终止字符(默认终止字符是EOF)时提前结束。

9.5 文件输入/输出

当涉及文件输入和输出时,C++ 提供了 头文件中的几个类和函数,用于创建和操作文件流对象。

  1. ofstream 类:用于创建输出文件流对象,用于写入数据到文件中。常用的成员函数有:

    • open:打开文件。可以接受文件名和打开模式作为参数。例如,open("output.txt") 打开名为 “output.txt” 的文件。
    • is_open:检查文件是否成功打开。返回一个布尔值,表示文件是否成功打开。
    • close:关闭文件。
  2. ifstream 类:用于创建输入文件流对象,用于从文件中读取数据。常用的成员函数有:

    • open:打开文件。可以接受文件名和打开模式作为参数。例如,open("input.txt") 打开名为 “input.txt” 的文件。
    • is_open:检查文件是否成功打开。返回一个布尔值,表示文件是否成功打开。
    • close:关闭文件。
  3. fstream 类:用于创建输入输出文件流对象,可以同时进行读取和写入操作。常用的成员函数有:

    • open:打开文件。可以接受文件名和打开模式作为参数。例如,open("data.txt") 打开名为 “data.txt” 的文件。
    • is_open:检查文件是否成功打开。返回一个布尔值,表示文件是否成功打开。
    • close:关闭文件。
  4. 文件打开模式:在打开文件时,可以指定打开模式来控制文件的读写行为。常用的打开模式有:

    • std::ios::in:以读取模式打开文件,用于输入操作。
    • std::ios::out:以写入模式打开文件,用于输出操作。如果文件不存在,则创建新文件;如果文件已存在,则清空文件内容。
    • std::ios::app:以追加模式打开文件,用于在文件末尾追加数据。
    • std::ios::binary:以二进制模式打开文件,用于处理二进制数据。
    • 可以通过使用按位或运算符 | 来同时指定多个打开模式。例如,std::ios::in | std::ios::binary 表示以二进制模式打开文件进行读取。

下面是一个示例,展示了如何使用文件流对象进行文件输入和输出:

#include 
#include 

int main() {
    // 创建输出文件流对象,打开文件 "output.txt"
    std::ofstream outputFile("output.txt");

    if (outputFile.is_open()) {
        // 向文件写入数据
        outputFile << "Hello, World!" << std::endl;

        // 关闭文件流
        outputFile.close();
    } else {
        std::cout << "Failed to open the file for writing." << std::endl;
    }

    // 创建输入文件流对象,打开文件 "input.txt"
    std::ifstream inputFile("input.txt");

    if (inputFile.is_open()) {
        std::string line;
        // 逐行读取文件内容
        while (std::getline(inputFile, line)) {
            // 输出读取的内容
            std::cout << line << std::endl;
        }

        // 关闭文件流
        inputFile.close();
    } else {
        std::cout << "Failed to open the file for reading." << std::endl;
    }

    return 0;
}

在上述示例中,首先创建了一个输出文件流对象 outputFile,并使用 ofstream 类打开名为 “output.txt” 的文件。然后,使用 << 运算符将数据写入文件。最后,关闭文件流。

接下来,创建了一个输入文件流对象 inputFile,并使用 ifstream 类打开名为 “input.txt” 的文件。使用 std::getline 函数逐行读取文件内容,并将读取的内容输出到标准输出。最后,关闭文件流。

需要注意的是,文件流对象在使用之前需要检查是否成功打开文件。可以使用 is_open 函数来检查文件是否成功打开。如果文件打开成功,可以进行读取或写入操作;否则,需要处理文件打开失败的情况。

总之,通过使用文件流对象和相应的成员函数,可以在C++中进行文件输入和输出操作。

十.异常处理与命名空间

10.1 异常处理

程序中常见的错位分为两大类:编译时错误和运行时错误。编译时的错误主要是语法错误,如关键字拼写错误、语句末尾缺分号、括号不匹配等。运行时出现的错误统称为异常,对异常的处理称为异常处理。

C++处理异常的办法:如果在执行一个函数的过程中出现异常,可以不在本函数中立即处理,而是发出一个信息,传给它的上一级(即调用函数)来解决,如果上一级函数也不能处理,就再传给其上一级,由其上一级处理。如此逐级上传,如果到最高一级还无法处理,运行系统一般会自动调用系统函数terminate(),由它调用abort终止程序。

异常处理是一种在程序执行过程中检测和处理错误的机制。在C++中,异常处理通过使用 try-catch 块来捕获和处理异常。

下面是一个基本的异常处理示例:

#include 

int main() {
    try {
        // 可能抛出异常的代码
        int dividend = 10;
        int divisor = 0;
        int result = dividend / divisor;
        std::cout << "Result: " << result << std::endl;
    } catch (const std::exception& e) {
        // 异常处理代码
        std::cout << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

在上述示例中,代码尝试将变量 dividend 除以变量 divisor,而 divisor 的值为零,这会导致异常。在 try 块中,包含可能抛出异常的代码。如果异常发生,程序会立即跳转到与异常类型匹配的 catch 块。

catch 块中,可以使用参数来接收异常对象。在示例中,使用 const std::exception& e 接收异常对象。std::exception 是C++标准库中的基本异常类,可以提供有关异常的信息。可以使用 e.what() 访问异常的具体信息。

除了 std::exception,C++标准库还提供了其他异常类,如 std::runtime_errorstd::logic_error 等,用于处理特定类型的异常。可以根据实际情况选择合适的异常类。

可以使用多个 catch 块来处理不同类型的异常。异常处理会按照 catch 块的顺序依次匹配异常类型,直到找到匹配的 catch 块为止。如果没有找到匹配的 catch 块,异常将继续向上层调用栈传播,直到找到合适的异常处理代码或程序终止。

除了 catch 块,还可以使用 throw 语句主动抛出异常。throw 语句用于在程序中的任何位置抛出异常,并将控制权传递给匹配的 catch 块。

#include 

void processValue(int value) {
    if (value < 0) {
        throw std::runtime_error("Invalid value");
    }
    // 其他处理逻辑
}

int main() {
    try {
        int value = -5;
        processValue(value);
    } catch (const std::exception& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

在上述示例中,processValue 函数接收一个整数参数 value,如果 value 的值小于零,会抛出一个 std::runtime_error 类型的异常。在 main 函数中,调用 processValue 函数并捕获异常。

异常处理是一种在程序执行过程中检测和处理错误的机制。通过使用 try-catch 块,可以捕获和处理异常。可以使用多个 catch 块来处理不同类型的异常,也可以使用 throw 语句主动抛出异常。异常处理可以帮助程序更好地处理错误情况,并提供相应的错误处理逻辑。

**例子:**输入三角形的三条边长,求三角形的面积。当输入边的长度小于0时,或者当三条边都大于0时但不能构成三角形时,分别抛出异常,结束程序运行。

#include 
#include 
using namespace std;

double triangle(double a, double b, double c) {
	double s = (a + b + c) / 2;
	if (a + b <= c || a + c <= b || b + c <= a) {
		throw 1.0;        //语句throw抛出double异常
	}
	return sqrt(s * (s - a) * (s - b) * (s - c));
}

int main() {
	double a, b, c;
	try {
		cout << "请输入三角形的三个边长(a, b, c): " << endl;
		cin >> a >> b >> c;
		if (a < 0 || b < 0 || c < 0) {
			throw 1;   //语句throw抛出int异常
		}
		while (a > 0 && b > 0 && c > 0) {
			cout << "a = " << a << " b = " << b << " c = " << c << endl;
			cout << "三角形的面积 = " << triangle(a, b, c) << endl;
			cin >> a >> b >> c;
			if (a <= 0 || b <= 0 || c <= 0) {
				throw 1;
			}
		}
	} catch (double) {
		cout << "这三条边不能构成三角形..." << endl;
	} catch (int) {
		cout << "边长小于或等于0..." << endl;
	}
	return 0;
}

10.2 命名空间和头文件命名规则

命名空间:一个由程序设计者命名的内存区域。程序设计者可以根据需要指定一些有名字的命名空间,将各命名空间中声明的标识符与该命名空间标识符建立关联,保证不同命名空间的同名标识符不发生冲突。

1.带扩展名的头文件的使用
在C语言程序中头文件包括扩展名.h,使用规则如下面例子
    #include 
2.不带扩展名的头文件的使用
C++标准要求系统提供的头文件不包括扩展名.h,如string,string.h等。
    #include 
#include 

// 定义一个命名空间
namespace MyNamespace {
    int x = 5;

    void printX() {
        std::cout << "x = " << x << std::endl;
    }
}

int main() {
    // 访问命名空间中的成员
    MyNamespace::printX();

    return 0;
}

在上述示例中,首先定义了一个命名空间 MyNamespace,其中包含一个整型变量 x 和一个函数 printX。在 main 函数中,通过 MyNamespace::printX() 访问了命名空间中的函数。

使用命名空间的好处是可以避免命名冲突。当存在多个不同的代码模块,每个模块都有一个名为 x 的变量时,可以将它们放置在不同的命名空间中,以避免冲突。通过使用命名空间限定符 ::,可以明确指定要访问的成员所属的命名空间。

此外,还可以使用 using 声明来简化对命名空间成员的访问。例如,可以使用 using MyNamespace::x 来直接访问命名空间中的变量 x

#include 

namespace MyNamespace {
    int x = 5;
}

int main() {
    using MyNamespace::x;

    std::cout << "x = " << x << std::endl;

    return 0;
}

上述示例中,通过 using MyNamespace::x 将命名空间 MyNamespace 中的变量 x 引入到当前作用域,可以直接使用 x 访问该变量。

总之,命名空间是C++中的一种机制,用于组织代码和避免命名冲突。通过将相关的函数、类、变量等放置在命名空间中,可以将它们封装在一个独立的作用域中,并使用命名空间限定符 ::using 声明来访问其中的成员。

十一.STL标准模板库

标准模板库Standard Template Library)中包含了很多实用的组件,利用这些组件,程序员编程方便而高效。

11.1Vector

vector容器与数组类似,包含一组地址连续的存储单元。对vector容器可以进行很多操作,包括查询、插入、删除等常见操作。

std::vector 是 C++ 标准库中的一个容器类,提供了动态数组的功能。它可以根据需要自动调整大小,并提供了方便的成员函数和操作符,用于对数组进行操作。

要使用 std::vector,需要包含头文件 。下面是一个简单的示例,展示了如何使用 std::vector

#include 
#include 

int main() {
    // 创建一个空的 vector
    std::vector<int> numbers;

    // 添加元素到 vector
    numbers.push_back(10);
    numbers.push_back(20);
    numbers.push_back(30);

    // 使用下标访问元素
    std::cout << "First element: " << numbers[0] << std::endl;
    std::cout << "Second element: " << numbers[1] << std::endl;

    // 使用迭代器遍历 vector
    std::cout << "Elements: ";
    for (std::vector<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 获取 vector 的大小
    std::cout << "Size: " << numbers.size() << std::endl;

    // 清空 vector
    numbers.clear();

    // 检查 vector 是否为空
    if (numbers.empty()) {
        std::cout << "Vector is empty" << std::endl;
    }

    return 0;
}

在上面的示例中,我们创建了一个空的 std::vector 对象 numbers,然后使用 push_back 函数向其中添加了三个整数元素。我们可以使用下标或迭代器访问和遍历 vector 中的元素。size 函数返回 vector 的大小,clear 函数清空 vector 中的所有元素,empty 函数检查 vector 是否为空。

运行示例代码会输出以下结果:

First element: 10
Second element: 20
Elements: 10 20 30 
Size: 3
Vector is empty

可以看到,通过使用 std::vector,我们可以方便地向动态数组中添加、访问和删除元素,并且不需要手动管理内存。std::vector 还提供了许多其他有用的成员函数和操作符,例如插入元素、删除元素、排序等,可以根据具体需求进行使用。

11.2 list容器

std::list 是 C++ 标准库中的一个容器类,提供了双向链表的功能。它支持在链表的任意位置进行高效的插入和删除操作,但访问元素的效率相对较低。

要使用 std::list,需要包含头文件 。下面是一个简单的示例,展示了如何使用 std::list

#include 
#include 

int main() {
    // 创建一个空的 list
    std::list<int> numbers;

    // 添加元素到 list
    numbers.push_back(10);
    numbers.push_back(20);
    numbers.push_front(5);

    // 使用迭代器遍历 list
    std::cout << "Elements: ";
    for (std::list<int>::iterator it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 获取 list 的大小
    std::cout << "Size: " << numbers.size() << std::endl;

    // 在指定位置插入元素
    std::list<int>::iterator it = numbers.begin();
    ++it; // 移动到第二个元素的位置
    numbers.insert(it, 15);

    // 删除指定元素
    numbers.remove(20);

    // 使用迭代器逆序遍历 list
    std::cout << "Reverse elements: ";
    for (std::list<int>::reverse_iterator rit = numbers.rbegin(); rit != numbers.rend(); ++rit) {
        std::cout << *rit << " ";
    }
    std::cout << std::endl;

    return 0;
}

在上面的示例中,我们创建了一个空的 std::list 对象 numbers,然后使用 push_backpush_front 函数向其中添加了三个整数元素。我们可以使用迭代器遍历 list 中的元素。size 函数返回 list 的大小,insert 函数在指定位置插入元素,remove 函数删除指定的元素。注意,std::list 不支持使用下标访问元素,因为它不是连续存储的。

运行示例代码会输出以下结果:

Elements: 5 10 20 
Size: 3
Reverse elements: 20 10 5 

可以看到,通过使用 std::list,我们可以方便地在链表的任意位置插入和删除元素,并且不需要移动其他元素。但是,访问元素的效率相对较低,因为需要通过迭代器遍历链表。std::list 还提供了许多其他有用的成员函数和操作符,例如排序、合并、去重等,可以根据具体需求进行使用。

你可能感兴趣的:(c++)