掌握C++面向对象:从类的声明到运算符重载的完整指南

一、 类的声明

在C++中我们可以使用 classstruct 关键字来声明一个类,两种方式都可以。
下面我们来分别看一下:

1. 使用 class 和 struct 关键字声明类

两种方式声明的类在本质上是没有区别的,只是在默认情况下使用 class 关键字声明的类中的成员变量和成员函数都是 private 访问控制的,而使用 struct 关键字声明的类中的成员变量和成员函数默认是 public 访问控制的。

// 使用 class 关键字声明一个类
class Person {
    // 类的成员变量和成员函数在这里声明和定义
};

// 使用 struct 关键字声明一个类
struct Student {
    // 同样在这里声明和定义成员变量和成员函数
};

2. 类的成员变量和成员函数的声明和定义

下面我们来看看如何在类中声明和定义成员变量和成员函数:

在代码中先声明并定义了一个 Student 类,其中包含了三个私有成员变量 id_name_age_,以及两个公有成员函数 study()sleep()。在类的外部我们需要再次定义类的构造函数和成员函数,使用 :: 运算符来表示这个函数和类之间的关系。

class Student {
    // 类的成员变量
    private:
        int id_;
        string name_;
        int age_;
    public:
        // 类的构造函数
        Student(int id, string name, int age);
        // 类的成员函数
        void study();
        void sleep();
};

// 类的构造函数的定义
Student::Student(int id, string name, int age) {
    id_ = id;
    name_ = name;
    age_ = age;
}

// 类的成员函数的定义
void Student::study() {
    cout << name_ << "正在学习..." << endl;
}

void Student::sleep() {
    cout << name_ << "正在睡觉..." << endl;
}

3. 类的访问控制:public、private、protected

在C++ 中类的成员变量和成员函数可以分别使用 publicprivateprotected 访问控制符来设定其访问权限。其中:

  • public 成员变量和成员函数可以在类的外部被直接访问;
  • private 成员变量和成员函数只能在类的内部被访问;
  • protected 成员变量和成员函数可以在类的内部和派生类中被访问。

下面是一个使用访问控制符的示例:
在代码中使用访问控制符分别设定了成员变量和成员函数的访问权限,其中 name_age_ 是私有成员变量只能在类的内部被访问。setName()getName() 是公有成员函数,可以在类的外部被访问。id_setId() 是保护成员变量和成员函数只能在类的内部和派生类中被访问。

class Person {
    // 私有成员变量,只能在类的内部被访问
    private:
        string name_;
        int age_;
    // 公有成员变量,可以在类的外部被访问
    public:
        // 公有成员函数,可以在类的外部被访问
        void setName(string name) {
            name_ = name;
        }
        // 公有成员函数,可以在类的外部被访问
        string getName() {
            return name_;
        }
    // 保护成员变量,只能在类的内部和派生类中被访问
    protected:
        int id_;
        void setId(int id) {
            id_ = id;
        }
};

二、 类的使用

在C++中面向对象编程是一种很重要的编程方式,对象是面向对象编程的基础。
下面将介绍如何使用 C++ 定义和使用对象,包括使用类来创建对象和访问对象的成员函数和数据成员

1. 使用类名创建对象

在C++中使用类名和构造函数可以创建一个对象。
下面的示例代码演示如何使用类名创建一个 Person 对象

在示例代码中首先定义了一个 Person 类,包括一个构造函数和一个成员函数。然后在 main 函数中使用类名和构造函数创建了一个 Person 对象 p。最后调用了 p 对象的成员函数 showInfo() 来输出 p 对象的属性信息

// 定义一个Person类
class Person {
public:
    // 声明并定义一个构造函数,用于初始化Person的属性
    Person(string name, int age) {
        name_ = name;
        age_ = age;
    }

    // 声明并定义一个公有成员函数,用于输出Person的属性
    void showInfo() {
        cout << "name: " << name_ << ", age: " << age_ << endl;
    }

private:
    string name_;  // 人名
    int age_;      // 年龄
};

// 在main函数中使用Person类创建一个对象
int main() {
    // 使用类名和构造函数创建一个对象
    Person p("Tom", 18);
    // 调用对象的成员函数
    p.showInfo();
    return 0;
}

2. 对象的数据成员和成员函数的访问

在C++中使用对象名和访问符号 . 来访问对象的数据成员和成员函数

下面的示例代码演示了如何访问对象的数据成员和成员函数:
在代码中创建一个对象 p,然后使用 . 访问符号来访问对象的数据成员 name_age_,并使用 . 访问符号来访问对象的成员函数 showInfo()。需要注意的是因为 name_age_ 是类的私有成员,所以不能直接在对象外部访问,而要通过对象的公有成员函数来访问

int main() {
    // 使用类名和构造函数创建一个对象
    Person p("Tom", 18);
    // 访问对象的数据成员
    cout << "name: " << p.name_ << ", age: " << p.age_ << endl;
    // 访问对象的成员函数
    p.showInfo();
    return 0;
}

三、 面向对象三大特性

1 继承

1.1 基类和派生类的定义

在C++ 中可以使用关键字 class 来定义类,并使用关键字 publicprivateprotected 来指定成员变量与成员函数的访问级别。

下面的示例代码演示了如何定义一个基类 Person 和一个派生类 Student

在示例代码中使用 class 定义了一个基类 Person,包括 nameage 两个公有成员变量。接着使用 classpublic 关键字定义了一个派生类 Student,并通过 : 指定了基类是 Person

// 定义一个基类 Person
class Person {
public:
    string name;
    int age;
};

// 定义一个派生类 Student
class Student : public Person {
public:
    int id;
};

1.2 继承的访问控制:public、private、protected

在 C++ 中可以使用 publicprivateprotected 来控制派生类对基类成员的访问权限。

下面的示例代码展示了这三种访问控制的用法:

// 定义一个基类 Person
class Person {
public:
    string name;
    int age;
private:
    string idCard;
protected:
    string phoneNum;
};

// 定义一个类 Student,public 继承 Person 类,可以访问 Person 类的 public 成员和 protected 成员,
// 但不能访问 Person 类的 private 成员
class Student : public Person {
public:
    void showInfo() {
        // 可以访问 Person 的 public 成员变量 name 和 age
        cout << "name: " << name << ", age: " << age << endl;
        // 可以访问 Person 的 protected 成员变量 phoneNum
        cout << "phoneNum: " << phoneNum << endl;
        // 不能访问 Person 的 private 成员变量 idCard,因为它是私有的
        // cout << "idCard: " << idCard << endl; // 错误示例
    }
};

// 定义一个类 Teacher,private 继承 Person 类,无法访问 Person 类的 public 和 protected 成员,
// 更不能访问 Person 类的 private 成员
class Teacher : private Person {
public:
    void showInfo() {
        // 不能访问 Person 的 public 成员变量 name 和 age
        // cout << "name: " << name << ", age: " << age << endl; // 错误示例
        // 不能访问 Person 的 protected 成员变量 phoneNum
        // cout << "phoneNum: " << phoneNum << endl; // 错误示例
        // 不能访问 Person 的 private 成员变量 idCard,因为它是私有的
        // cout << "idCard: " << idCard << endl; // 错误示例
    }
};

1.3 多重继承和虚继承

在 C++ 中一个类可以从多个基类派生而来(多重继承)。同时如果多个派生类都继承自同一个基类,可以使用虚继承来避免基类在派生树中的重复实例化。

多重继承的定义方法和单一继承类似,只需要在类定义中有多个用逗号隔开的基类即可。

下面的示例代码演示了如何使用多重继承:

// 定义一个基类 Animal
class Animal {
public:
    void eat() {
        cout << "Animal::eat()\n";
    }
};

// 定义一个基类 Person
class Person {
public:
    void eat() {
        cout << "Person::eat()\n";
    }
};

// 定义一个派生类 Worker,public 继承自 Animal 和 Person
class Worker : public Animal, public Person {
public:
    void doWork() {
        cout << "Worker::doWork()\n";
    }
};

// 定义一个派生类 Student
class Student : public Person {
public:
    void study() {
        cout << "Student::study()\n";
    }
};

// 定义一个派生类 Graduate,public 继承自 Person,虚继承自 Animal
class Graduate : public Person, virtual public Animal {
public:
    void research() {
        cout << "Graduate::research()\n";
    }

    // 重写 Animal 类的 eat() 函数
    void eat() {
        cout << "Graduate::eat()\n";
    }
};

2 封装

2.1 数据封装:使用 private 成员变量和 public 成员函数限制访问

封装是指将某些成员变量和成员函数进行私有化,并提供公共接口来访问这些私有成员。这种方式可以保证类内部访问的安全性和稳定性,同时提高了代码的可维护性。

在 C++ 中可以使用 private 来定义私有成员变量和成员函数,使用 public 来定义公有成员函数。

下面的示例代码展示了如何使用封装:

class Person {
private:
    string name;
    int age;

public:
    // 公有构造函数
    Person(string _name, int _age) : name(_name), age(_age) {
    }

    // 公有成员函数
    void showInfo() {
        cout << "name: " << name << ", age: " << age << endl;
    }
};

int main() {
    // 创建一个 Person 对象,并调用公有成员函数 showInfo()
    Person p("Bob", 20);
    p.showInfo();

    // 不能直接访问私有成员变量 name 和 age
    // cout << p.name << endl; // 错误示例
    // cout << p.age << endl; // 错误示例

    return 0;
}

2.2 函数封装:使用成员函数进行操作

函数封装指的是将同一功能的代码封装进一个函数中,并在需要的时候调用该函数来完成相应的操作。在面向对象编程中可以使用成员函数来进行函数封装。

下面的示例代码演示了如何使用成员函数进行函数封装:

class Rectangle {
private:
    int width;
    int height;

public:
    // 构造函数
    Rectangle(int _width, int _height) : width(_width), height(_height) {
    }

    // 公有成员函数,计算矩形面积
    int getArea() {
        return width * height;
    }
};

int main() {
    // 创建一个 Rectangle 对象
    Rectangle r(10, 20);

    // 调用成员函数计算矩形面积
    int area = r.getArea();
    cout << "area: " << area << endl;

    return 0;
}

3 多态

3.1 虚函数和纯虚函数的定义和实现

多态是指同一个类型的变量在不同的情况下有不同的表现形式。在C++ 中多态性主要通过虚函数和纯虚函数来实现。

虚函数是指在基类中用 virtual 声明的成员函数,在派生类中可以被重新定义,实现同名的函数有不同的行为。纯虚函数是指在基类中用 virtual 声明的没有实际函数体的函数,又称为接口函数。当一个类包含纯虚函数时,它也成为抽象类无法被实例化。

下面的示例代码演示了如何使用虚函数和纯虚函数来实现多态:

// 定义一个抽象类 Shape
class Shape {
public:
    // 纯虚函数,表示这个函数是接口函数,没有实际意义
    virtual double getArea() = 0;
};

// 定义一个派生类 Rectangle
class Rectangle : public Shape {
private:
    double width;
    double height;

public:
    Rectangle(double _width, double _height) : width(_width), height(_height) {
    }

    // 实现基类的虚函数 getArea()
    double getArea() {
        return width * height;
    }
};

// 定义一个派生类 Circle
class Circle : public Shape {
private:
    double radius;

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

    // 实现基类的虚函数 getArea()
    double getArea() {
        return 3.14 * radius * radius;
    }
};

int main() {
    // 创建一个基类指针,并分别指向 Rectangle 和 Circle 对象
    Shape* s1 = new Rectangle(10.0, 20.0);
    Shape* s2 = new Circle(5.0);

    // 调用基类的虚函数 getArea(),实际上调用的是派生类的实现
    double area1 = s1->getArea();
    double area2 = s2->getArea();

    cout << "area1: " << area1 << endl;
    cout << "area2: " << area2 << endl;

    return 0;
}

3.2 动态多态性的实现:使用基类指针调用派生类虚函数

动态多态性是指在运行期间根据对象的类型关系进行函数调用,这种多态性又称为运行时多态性或后期绑定。在C++ 中动态多态性通常通过基类的指针或引用调用派生类的虚函数来实现。

下面的示例代码演示了如何使用基类指针调用派生类的虚函数来实现动态多态性:

// 定义一个抽象类 Animal
class Animal {
public:
    // 虚函数,表示这个函数可以被派生类重新定义
    virtual void speak() = 0;
};

// 定义一个派生类 Dog
class Dog : public Animal {
public:
    void speak() {
        cout << "This is a dog" << endl;
    }
};

// 定义一个派生类 Cat
class Cat : public Animal {
public:
    void speak() {
        cout << "This is a cat" << endl;
    }
};

int main() {
    // 创建一个基类指针,并分别指向 Dog 和 Cat 对象
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    // 调用基类的虚函数 speak(),实际调用的是派生类的实现
    animal1->speak();
    animal2->speak();

    return 0;
}

3.3 静态多态性的实现:函数重载和模板

静态多态性是指在编译期间根据对象的类型关系进行函数调用,这种多态性又称为编译时多态性或早期绑定。在C++ 中静态多态性通常通过函数重载和模板来实现。

下面的示例代码演示了如何使用函数重载和模板来实现静态多态性:

在调用函数模板时,编译器会根据参数类型自动推断出模板的实例化类型,并生成对应的代码。而在调用函数重载时,编译器会根据函数参数的类型和个数来判断应该调用哪个具体的函数实现。

// 定义一个函数模板,实现两个数相加
template<typename T>
T add(T a, T b) {
    return a + b;
}

// 定义一个函数重载,实现两个字符串拼接
string add(string a, string b) {
    return a + " " + b;
}

int main() {
    // 调用函数模板 add,实现两个整数相加
    int result1 = add<int>(10, 20);
    cout << "result1: " << result1 << endl;

    // 调用函数模板 add,实现两个浮点数相加
    double result2 = add<double>(3.14, 2.71);
    cout << "result2: " << result2 << endl;

    // 调用函数重载 add,实现两个字符串拼接
    string result3 = add("Hello", "world!");
    cout << "result3: " << result3 << endl;

    return 0;
}

四、抽象类和接口

抽象类和接口是面向对象编程中的两个重要概念,下面将详细介绍抽象类和接口的定义和作用

1 抽象类的定义和作用

抽象类是含有至少一个纯虚函数的类。所谓纯虚函数就是在函数声明后加上”=0”,表示该函数没有实现,需要在派生类中实现。抽象类的特点如下:

  • 抽象类不能被实例化,只能作为其他类的基类来使用。
  • 继承自抽象类的子类必须实现所有纯虚函数,否则该子类也会变成抽象类。
  • 抽象类的作用在于提供一种规范,约束了子类必须实现特定的接口,同时具有良好的拓展性。

以下是定义一个抽象类的示例代码:

在代码中Animal类含有一个纯虚函数sayHi()和一个普通函数eat()。由于sayHi()是纯虚函数,因此Animal类是一个抽象类

class Animal {
public:
    // 定义纯虚函数,不需要实现
    virtual void sayHi() = 0;

    // 普通函数,需要实现
    void eat() {
        cout << "Animal is eating." << endl;
    }
};

2 接口类的定义和作用

接口类是由全是纯虚函数的抽象类组成的。接口类中所有的函数都是纯虚函数,没有实现。接口类的特点如下:

  • 继承自接口类的子类必须实现接口类中的所有函数,否则该子类也会变成抽象类。
  • 接口类的作用在于提供一种统一的接口规范,便于维护和拓展。

以下是定义一个接口类的示例代码:

在代码中Shape类是接口类,其中的getArea()和getPerimeter()函数都是纯虚函数

class Shape {
public:
    // 定义纯虚函数,不需要实现
    virtual double getArea() = 0;
    virtual double getPerimeter() = 0;
};

通过抽象类和接口类的使用我们可以通过约束子类必须实现特定的接口规范,来降低代码的耦合度和复杂度,提高可拓展性和维护性。

五、 STL(标准模板库)

Standard Template Library是C++中非常重要的一个标准库,为开发人员提供丰富的数据结构和算法

1 STL容器

STL中的容器是用于存储和管理数据的一种数据结构。STL中的容器按照其内部实现方式可分为如下三种类型:

  1. 顺序容器(Sequence Containers):元素在容器内部按照添加的顺序排列

    • 包括vector、list、deque和array等类型。
  2. 关联容器(Associative Containers):元素在容器内部按照某种规则自动排列

    • 包括set、map、multiset和multimap等类型。
  3. 无序容器(Unordered Containers):元素在容器内部的存储位置不是固定不变的,具有可变性和自适应性

    • 包括unordered_set和unordered_map等类型。

接下来我们将按照上述分类分别介绍各种容器和其使用方法。

1.1 顺序容器

1.1.1 vector

vector是STL中最常见的顺序容器之一,具体特点如下:

  • 动态数组,可动态扩展容量;
  • 内部实现使用连续存储,支持随机访问;
  • 支持在尾部添加和删除元素,也支持在指定位置插入和删除元素;
  • 插入和删除元素时如果需要内部扩容或减少容量,则会使用“倍增”的方式,避免了大量的内存重分配。

以下是vector的常见操作:

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

int main() {
  vector<int> nums; //定义一个vector,存储int类型的数据
  nums.push_back(1); //在vector尾部添加元素
  nums.push_back(2);
  nums.push_back(3);

  //通过索引遍历vector
  for (int i = 0; i < nums.size(); ++i) {
    cout << nums[i] << " ";
  }
  cout << endl;

  //通过指针遍历vector
  for (vector<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;

  //删除vector尾部的元素
  nums.pop_back();

  //在指定位置插入元素
  nums.insert(nums.begin() + 1, 4);

  //在指定位置删除元素
  nums.erase(nums.begin() + 1);

  return 0;
}
1.1.2 list

list是STL中的另一个顺序容器,内部实现是双向链表,支持在头部、尾部、指定位置添加和删除元素,但不支持随机访问。
以下是list的常见操作:

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

int main() {
  list<int> nums; //定义一个list,存储int类型的数据
  nums.push_back(1); //在list尾部添加元素
  nums.push_front(2); //在list头部添加元素,注意此时的大小
  nums.push_back(3);
  nums.push_front(4);

  //通过迭代器遍历list
  for (list<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;

  //在指定位置插入元素
  list<int>::iterator it = nums.begin();
  it++; //指向第一个元素
  nums.insert(it, 5);

  //在指定位置删除元素
  it++; //指向第二个元素
  nums.erase(it);

  return 0;
}
1.1.3 deque

deque代表双端队列(Double-Ended Queue),也是一种顺序容器,其内部实现是一个中央控制器,维护了一段连续的存储空间,称之为map,其中每个元素是一个定长数组,称之为buffer。deque支持在头部和尾部添加和删除元素。

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

int main() {
  deque<int> nums; //定义一个deque,存储int类型的数据
  nums.push_back(1); //在尾部添加元素
  nums.push_front(2); //在头部添加元素
  nums.push_back(3);
  nums.push_front(4);

  //通过迭代器遍历deque
  for (deque<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;

  //在指定位置插入元素
  deque<int>::iterator it = nums.begin() + 2;
  nums.insert(it, 5);

  //在指定位置删除元素
  it++; //指向第三个元素
  nums.erase(it);

  return 0;
}
1.1.4 array

array是一种顺序容器,用于存储元素数量固定的数组。该容器使用连续的存储空间存储元素,可支持随机访问不支持添加和删除操作

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

int main() {
  array<int, 4> nums
//定义一个大小为4的array,存储int类型的数据
  nums.fill(0); //初始化元素全部为0

  //基于范围的for循环遍历array
  for (int& num : nums) {
    num = rand() % 100; //随机生成0~99之间的整数
  }

  //通过索引遍历array
  for (int i = 0; i < nums.size(); ++i) {
    cout << nums[i] << " ";
  }
  cout << endl;

  //在指定位置访问和修改元素
  nums.at(2) = 100;
  cout << nums.at(2) << endl;

  return 0;
}

1.2 关联容器

1.2.1 set

set是一种关联容器,用于存储不重复的元素集合。set内部实现采用红黑树可自动将元素进行排序,支持查找、插入和删除操作

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

int main() {
  set<int> nums; //定义一个set,用于存储int类型的数据
  nums.insert(1); //向set中插入元素
  nums.insert(2);
  nums.insert(3);
  nums.insert(2); //set不允许插入重复元素,因此该操作无效

  //通过迭代器遍历set
  for (set<int>::iterator it = nums.begin(); it != nums.end(); ++it) {
    cout << *it << " ";
  }
  cout << endl;

  //查找元素
  set<int>::iterator it = nums.find(2);
  if (it != nums.end()) {
    cout << "Found " << *it << endl;
  } else {
    cout << "Not Found" << endl;
  }

  //删除元素
  nums.erase(2);

  return 0;
}
1.2.2 map

map是一种关联容器,也是一种集合,用于存储键值对(Key-Value Pair)。map内部实现也采用红黑树,可自动将元素按照Key进行排序,支持查找、插入和删除操作

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

int main() {
  map<char, int> dict; //定义一个map,用于存储char类型和int类型的键值对
  dict.insert(make_pair('A', 65)); //向map中插入键值对
  dict.insert(make_pair('B', 66));
  dict.insert(make_pair('C', 67));
  dict.insert(make_pair('B', 68)); //map不允许插入重复Key值,因此该操作无效

  //通过迭代器遍历map
  for (map<char, int>::iterator it = dict.begin(); it != dict.end(); ++it) {
    cout << it->first << ": " << it->second << endl;
  }

  //查找元素
  map<char, int>::iterator it = dict.find('B');
  if (it != dict.end()) {
    cout << "Found " << it->first << ": " << it->second << endl;
  } else {
    cout << "Not Found" << endl;
  }

  //删除元素
  dict.erase('B');

  return 0;
}
1.2.3 multiset 与 multimap

multiset和multimap是set和map的衍生容器,同样包含Key的排序和查找功能,但不要求元素唯一
使用方法和set和map类似,只需要将set和map各自的名称改为multiset和multimap即可。

六、异常处理

在编写程序时常常需要考虑出现异常的情况和异常的处理。C++ 中提供了try-catch 块和自定义异常类两种异常处理机制让我们可以更好地应对各种异常情况。

1 try-catch 块的使用

使用 try-catch 块来捕获和处理异常。try 块中包含可能会抛出异常的代码,而 catch 块则用于处理抛出的异常。当 try 块中的代码抛出异常时,程序会跳转到对应的 catch 块中,执行其中的代码从而实现异常处理。

catch 块中,ExceptionType 是捕获的异常类型,& e 是指向异常对象的引用。如果抛出的异常类型和 ExceptionType 相同,就会执行对应的 catch 块中的代码。同时,在 catch 块中可以通过 e 访问异常对象的属性和方法。

try {
  // 可能会抛出异常的代码
} catch (ExceptionType1& e) {
  // 异常类型为 ExceptionType1 时的处理
} catch (ExceptionType2& e) {
  // 异常类型为 ExceptionType2 时的处理
} catch (...) {
  // 其他异常类型的处理
}

需要注意的是在一个 try-catch 块中,可以有多个 catch 块,用以处理不同类型的异常。如果抛出的异常类型与第一个 catch 块中的类型不匹配,就会依次匹配下一个 catch 块,直到找到匹配的类型或者所有 catch 块都无法处理该异常为止

2 异常类的定义和使用

除了使用预定义的异常类型外,我们还可以自定义异常类来处理自定义的异常。这需要我们定义一个继承自 std::exception 的异常类,并在该类中实现必要的方法和属性。

在代码中定义了一个名为 MyException 的异常类,该类继承自 std::exception。我们通过重载 what 方法来设置异常信息。what 方法返回一个 const char* 类型的指针,该指针指向异常信息的字符串表示

class MyException : public exception {
 public:
  MyException(const char* message) : message_(message) {}
  const char* what() const throw() { return message_.c_str(); }

 private:
  string message_;
};

接下来我们可以在代码中使用 throw 关键字来抛出自定义异常:

在代码中e.what() 方法返回的是 const char* 类型的异常信息。我们可以使用 cerr 来输出该信息,从而帮助我们更好地理解和处理异常

if (error_condition) {
  throw MyException("something went wrong"); // 抛出 MyException 类型的异常
}

try-catch 块中可以按照上一节的示例代码来捕获和处理自定义异常。可以在 catch 块中调用自定义异常类的方法来获取异常信息,如下所示:

try {
  // 可能会抛出异常的代码
} catch (MyException& e) {
  cerr << "Exception caught: " << e.what() << endl;
}

在实际的编程过程中要注意异常处理的效率和可靠性,避免因异常处理不当而带来更多的问题和后果

七、类型转换

在C++ 编程中经常需要进行数据类型的转换,C++ 提供了多种类型转换的方式,包括隐式类型转换和显示类型转换。
接下来我们就详细介绍这些转换方式:

1 隐式类型转换和显示类型转换

当我们使用不同的数据类型进行运算时,编译器会自动进行类型转换。如果操作数的类型不同,那么编译器会根据一定的规则,将较低精度的类型自动转换为较高精度的类型。

例如当我们对一个整数变量和一个浮点数变量进行加法运算时,编译器会自动将整数变量转换为浮点数,以适应浮点数变量的精度

int a = 10;
double b = 5.5;
double c = a + b; // a 会自动转换为 double 类型

除了上述情况C++ 还可以进行隐式类型转换,例如在赋值和函数调用等场景中,编译器会根据实际情况自动进行类型转换,并调整变量的类型。这就是隐式类型转换。

int a = 10;
char b = 'a';
char c = a; // a 会自动转换为 char 类型
int d = b; // b 会自动转换为 int 类型

与隐式类型转换相对的是显示类型转换。显示类型转换需要使用转换运算符,可以强制将一个数据类型转换为另一个数据类型。在C++ 中包含有四种类型转换运算符,分别是:static_castdynamic_castconst_castreinterpret_cast

2 static_cast、dynamic_cast、const_cast、reinterpret_cast 的使用

2.1 static_cast

static_cast 通常用于将一个表达式转换为某一种类型,它可以用于基本类型、指针类型和引用类型。

float a = 10.5;
int b = static_cast<int>(a); // 将 float 类型的 a 转换为 int 类型的 b

当我们需要将一个指针或引用转换为它所指向或所引用的类型时,也可以使用 static_cast ,如下所示:

int a = 10;
const void* p = &a;
int* q = static_cast<int*>(const_cast<void*>(p)); // 将 const void* 类型的 p 转换为 int* 类型的 q

需要注意的是static_cast 的转换只能进行编译时确定的转换,也就是说它只能进行相对安全的类型转换。如果转换的目标类型与原始类型不兼容,将会导致运行时错误。

2.2 dynamic_cast

dynamic_cast 通常用于将一个指针或引用转换为其继承类或基础类的类型。在使用 dynamic_cast 进行类型转换时,如果目标类型与原始类型不兼容,将会返回一个空指针或引用。因此我们需要进行空指针或引用的判断,避免因为转换失败而导致的运行时错误

class Base {};
class Derived : public Base {};

void foo(Base* p) {
  Derived* q = dynamic_cast<Derived*>(p); // 将 Base* 类型的 p 转换为 Derived* 类型的 q
  if (q != nullptr) {
    // 转换成功
  } else {
    // 转换失败
  }
}

需要注意的是dynamic_cast 通常用于运行时类型信息的转换,并不适合于所有的类型转换

2.3 const_cast

const_cast 通常用于将一个 const 类型的变量转换为非 const 类型。需要注意的是const_cast 用于去除 const 属性,而不能用于添加 const 属性。

const int a = 10;
int b = const_cast<int&>(a); // 将 const int 类型的 a 转换为 int 类型的 b

2.4 reinterpret_cast

reinterpret_cast 通常用于将一个指针或引用转换为与它有着相同二进制数据的另一种类型。这种类型转换是非常危险的,因为它会混淆不同类型的数据,导致程序出现逻辑错误和运行时错误,不建议在代码中频繁使用。

char* p = reinterpret_cast<char*>(&a); // 将 int* 类型的指针 p 转换为 char* 类型的指针 p

需要注意的是reinterpret_cast 通常用于底层编程,需要在使用时谨慎行事,保证类型转换的正确性。

八、运算符重载

在C++编程中运算符重载是一项非常重要的特性,它允许我们对已有的运算符进行重载,从而实现自定义的功能和行为。C语言中支持重载的运算符包括多种算术运算符、关系运算符、逻辑运算符以及成员访问运算符等等

1 成员函数重载运算符

C++支持将运算符重载为成员函数,这意味着可以在自定义的类中定义重载运算符,并使用这些运算符进行对象的操作。当我们使用一个成员函数重载运算符时,它的左操作数就是被调用该成员函数的对象本身,而右操作数则是该运算符的参数

下面是一个示例,展示了如何在类中重载加法运算符 +

在示例中创建了一个名为 Vector 的类,并在其中定义了加法运算符 + 的重载函数。该函数接受一个参数 v,该参数为 Vector 类型,返回一个新的 Vector 对象。在 main 函数中,我们创建了两个 Vector 类型的对象 v1v2,并使用重载的加法运算符将它们相加,将结果存储到 Vector v3 中。最后调用了 print 函数,打印了 v3 中存储的向量的坐标值

#include <iostream>

class Vector {
public:
  Vector(int x = 0, int y = 0) : x(x), y(y) {}
  
  // 重载加法运算符 "+"
  Vector operator+(const Vector& v) {
    int new_x = x + v.x;
    int new_y = y + v.y;
    return Vector(new_x, new_y);
  }
  
  // 打印向量的坐标值
  void print() {
    std::cout << "X: " << x << ", " << "Y: " << y << std::endl;
  }
  
private:
  int x;
  int y;
};

int main() {
  Vector v1(3, 5);
  Vector v2(2, 4);
  Vector v3 = v1 + v2;
  v3.print(); // X: 5, Y: 9
  return 0;
}

2 非成员函数重载运算符

除了成员函数之外还可以将运算符重载为非成员函数。非成员函数重载运算符的设计,可以使我们在自定义数据类型中使用标准运算符。与成员函数相比,重载运算符的非成员函数并不受限于当前类的作用域范围,可以在任何作用域内定义。

下面是一个示例展示了如何重载乘法运算符 * 为非成员函数:

在示例中创建了一个名为 Complex 的类,并在其中定义了乘法运算符 * 的非成员函数。该函数接受两个参数 c1c2,分别为 Complex 类型,返回一个新的 Complex 对象。在 main 函数中创建了两个 Complex 类型的对象 c1c2,并使用重载的乘法运算符将它们相乘,将结果存储到 Complex c3 中。最后,我们调用了 print 函数,打印了 c3 中存储的复数的实部和虚部

#include <iostream>

class Complex {
public:
  Complex(int real = 0, int imag = 0) : real(real), imag(imag) {}
  
  // 在类外重载乘法运算符 "*"
  friend Complex operator*(const Complex& c1, const Complex& c2) {
    int new_real = c1.real * c2.real - c1.imag * c2.imag;
    int new_imag = c1.real * c2.imag + c1.imag * c2.real;
    return Complex(new_real, new_imag);
  }
  
  // 打印复数的实部和虚部
  void print() {
    std::cout << "Real: " << real << ", " << "Imag: " << imag << std::endl;
  }
  
private:
  int real;
  int imag;
};

int main() {
  Complex c1(1, 2);
  Complex c2(2, 3);
  Complex c3 = c1 * c2;
  c3.print(); // Real: -4, Imag: 7
  return 0;
}
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

格林希尔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值