C++(2)进阶语法
C++(2)之进阶语法
Author: Once Day Date: 2024年9月20日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
全系列文章可参考专栏: 源码分析_Once-Day的博客-CSDN博客
参考文章:
- C++ 基本语法_w3cschool
- Learn C++ – Skill up with our free tutorials (learncpp.com)
- 词法约定 | Microsoft Learn
- GitHub - isocpp/CppCoreGuidelines: The C++ Core Guidelines are a set of tried-and-true guidelines, rules, and best practices about
coding in C++- C++(10)类语法分析(1)-CSDN博客
- C++(11)类语法分析(2)-CSDN博客
文章目录
- C++(2)之进阶语法
- 1. 面对对象编程
- 1.1 过程性编程和面对对象编程
- 1.2 派生类和基类
- 1.3 多态公有继承
- 1.4 虚函数工作原理
- 1.5 抽象基类ABC
- 2. 代码重用
- 2.1 接口和实现
- 2.2 私有继承
- 2.3 虚基类(多重继承)
- 3. 类模板
- 3.1 类模版定义和使用
- 3.2 递归使用(模版数组和非类型参数)
- 3.3 将模版用作参数
- 3.4 模板类和友元
- 3.5 模版别名
- 4. 特殊用法
- 4.1 嵌套类
- 4.2 异常机制
- 4.3 异常安全代码
- 4.3 RTTI(运行期类型)
- 4.5 类型转换符
1. 面对对象编程
参考文档:
- 继承 (C++) | Microsoft Learn
1.1 过程性编程和面对对象编程
C++是一种多范式编程语言,支持多种编程风格和思想。其中,过程性编程(Procedural Programming)和面向对象编程(Object-Oriented Programming,简称OOP)是C++的两大主要编程范式。
过程性编程是一种以过程(函数)为中心组织代码的编程范式。在过程性编程中,程序被视为一系列的过程调用,每个过程完成特定的任务。过程性编程强调算法和数据结构的设计,关注问题的解决步骤和流程。
C++支持过程性编程的特点包括:
- 函数:使用函数来封装一组相关的语句,完成特定的任务。函数接受输入参数,执行指定的操作,并可以返回结果。
- 数据类型:提供了丰富的内置数据类型,如整型、浮点型、字符型等,用于表示和操作数据。
- 控制结构:提供了各种控制结构,如条件语句(if-else)、循环语句(for、while)等,用于控制程序的执行流程。
- 指针和引用:支持指针和引用,允许直接访问和操作内存中的数据,提供了灵活而高效的内存管理机制。
过程性编程适用于解决简单的、线性的问题,强调算法的设计和实现。
面向对象编程是一种以对象为中心组织代码的编程范式。在面向对象编程中,程序被视为一组相互交互的对象,每个对象封装了数据和操作数据的方法。面向对象编程强调数据的组织和封装,关注问题域中的实体及其关系。
C++支持面向对象编程的特点包括:
- 类和对象:使用类来定义对象的蓝图,包括数据成员和成员函数。通过类的实例化,可以创建具体的对象。
- 封装:通过访问修饰符(如public、private)来控制类的数据和方法的可见性,实现数据隐藏和封装。
- 继承:支持类的继承,允许派生类继承基类的数据和方法,实现代码的复用和扩展。
- 多态:通过虚函数和动态绑定实现多态,允许在运行时根据对象的实际类型调用相应的方法。
- 运算符重载:允许为用户定义的类型重载运算符,使得对象可以像内置类型一样进行操作。
面向对象编程适用于解决复杂的、具有层次结构的问题,强调对问题域的抽象和建模。
1.2 派生类和基类
参考文档:
- C++ 继承_w3cschool
- 继承 (C++) | Microsoft Learn
- 单个继承 | Microsoft Learn
在C++中,派生类(derived class)和基类(base class)是面向对象编程(OOP)中非常重要的概念。它们之间的关系体现了继承(inheritance)的思想,即派生类可以继承基类的成员变量和成员函数,从而实现代码的复用和扩展。
(1) 访问权限:在派生类中,可以通过访问说明符来控制从基类继承的成员的访问权限。
C++提供了三种访问说明符:
- public(公有继承),基类的public和protected成员在派生类中保持原有访问权限。
- protected(保护继承),基类的public和protected成员在派生类中变为protected权限。
- private(私有继承),基类的public和protected成员在派生类中变为private权限。
需要注意的是,基类的private成员在派生类中是不可直接访问的,但派生类可以通过基类的公有或保护成员函数来间接访问这些私有成员。
(2) 初始化方式:派生类的构造函数需要负责初始化派生类自己的成员,同时也需要调用基类的构造函数来初始化继承自基类的成员。派生类构造函数可以在初始化列表中显式地调用基类的构造函数,并传递必要的参数。如果派生类没有显式调用基类构造函数,编译器会自动调用基类的默认构造函数(如果存在)。
下面是一个简单的示例,演示了派生类和基类的基本使用:
class Shape {
protected:
int width;
int height;
public:
Shape(int w, int h) : width(w), height(h) {}
virtual int getArea() { return 0; }
};
class Rectangle : public Shape {
public:
Rectangle(int w, int h) : Shape(w, h) {}
int getArea() override { return width * height; }
};
class Square : public Rectangle {
public:
Square(int size) : Rectangle(size, size) {}
};
int main() {
Rectangle rect(4, 5);
Square square(3);
std::cout << "Rectangle area: " << rect.getArea() << std::endl;
std::cout << "Square area: " << square.getArea() << std::endl;
return 0;
}
在上面的例子中,Shape
是基类,Rectangle
和Square
是派生类。Shape
类有两个protected成员变量width
和height
,以及一个虚函数getArea()
。Rectangle
类公有继承自Shape
类,并重写了getArea()
函数来计算矩形的面积。Square
类公有继承自Rectangle
类,它的构造函数只接受一个参数size
,并将其传递给Rectangle
类的构造函数来初始化宽度和高度。
1.3 多态公有继承
在C++中,多态(polymorphism)是面向对象编程的重要特性之一,它允许我们通过基类的指针或引用来调用派生类的方法,实现运行时的动态绑定。
(1) 派生类中重新定义基类的方法(重载),在派生类中,可以重新定义基类中的方法,这称为方法的重载(overriding)。当通过派生类对象调用重载的方法时,会执行派生类中的实现,而不是基类中的实现。这允许派生类根据自己的需求对基类的方法进行定制和扩展。
(2) 使用虚方法,为了实现多态,需要在基类中将要被派生类重载的方法声明为虚方法(virtual method)。虚方法的声明以关键字 “virtual” 开头,例如virtual void foo();
。当通过基类的指针或引用调用虚方法时,会根据实际对象的类型来执行相应的派生类中的实现,这称为动态绑定(dynamic binding)或运行时多态(runtime polymorphism)。
(3) 使用作用域解析运算符调用基类方法,有时,派生类重载了基类的方法,但仍然需要调用基类中的实现。在这种情况下,可以使用作用域解析运算符(::)来显式地调用基类的方法。例如,在派生类的方法中,可以通过BaseClass::foo();
来调用基类的 “foo” 方法。
(4) 虚析构函数,在使用多态时,经常会通过基类的指针来管理派生类对象。当删除这些对象时,如果基类的析构函数不是虚函数,则只会调用基类的析构函数,而不会调用派生类的析构函数,可能导致资源泄漏。因此,在设计基类时,如果预期会被用于多态,就应该将其析构函数声明为虚函数,即 “virtual ~BaseClass();”。这样可以确保在删除基类指针时,会先调用派生类的析构函数,然后再调用基类的析构函数,保证正确的清理和资源释放。
下面是一个示例,演示了多态公有继承的各个特点:
class Shape {
public:
virtual double getArea() {
return 0.0;
}
virtual void printInfo() {
std::cout << "This is a shape." << std::endl;
}
virtual ~Shape() {
std::cout << "Shape destructor called." << std::endl;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() override {
return 3.14159 * radius * radius;
}
void printInfo() override {
Shape::printInfo();
std::cout << "This is a circle." << std::endl;
}
~Circle() {
std::cout << "Circle destructor called." << std::endl;
}
};
int main() {
Shape* shape = new Circle(2.0);
std::cout << "Area: " << shape->getArea() << std::endl;
shape->printInfo();
delete shape;
return 0;
}
在这个例子中,Shape
是基类,其中的getArea
和printInfo
方法都是虚函数。Circle
是派生类,它重载了这两个方法。在main
函数中,创建了一个Circle
对象,但是通过Shape
的指针来管理它。当调用getArea
和printInfo
方法时,实际执行的是Circle
类中的实现,体现了多态的特性。
在Circle
的printInfo
方法中,使用作用域解析运算符Shape::printInfo()
来调用基类的实现,然后再输出一条特定于Circle
的信息。
最后,在Shape
和Circle
类中,都定义了虚析构函数。当通过delete shape;
删除对象时,会先调用Circle
的析构函数,然后再调用Shape
的析构函数,确保正确的清理和资源释放。
1.4 虚函数工作原理
参考文档:虚函数 | Microsoft Learn
虚函数是C++实现多态的重要机制,它允许通过基类的指针或引用来调用派生类的方法。
(1) 虚函数表(Virtual Function Table,vtable),当一个类包含虚函数时,编译器会为该类生成一个虚函数表。虚函数表是一个存储虚函数指针的数组,每个虚函数在表中都有一个对应的条目。每个包含虚函数的类对象都有一个指向虚函数表的指针(通常称为vptr),该指针在对象创建时自动设置。当通过指针或引用调用虚函数时,编译器会通过对象的vptr找到虚函数表,并从表中获取相应的函数指针来调用实际的函数实现。
(2) 静态绑定和动态绑定,静态绑定(static binding)是在编译时确定函数调用的目标,而动态绑定(dynamic binding)则是在运行时根据实际对象的类型来确定调用哪个函数。对于非虚函数,编译器使用静态联编,因为函数的调用目标在编译时就可以确定。对于虚函数,编译器使用动态联编,因为实际调用的函数要到运行时根据对象的实际类型来确定。动态联编是通过虚函数表来实现的。
(3) 重载函数与虚函数原型匹配,当派生类重载虚函数时,重载函数的原型(函数名、参数类型和顺序)必须与基类中虚函数的原型完全匹配。如果派生类的重载函数与基类的虚函数原型不匹配,那么派生类的函数将被视为一个新的函数,而不是对基类虚函数的重载。这可能导致意外的行为,因为通过基类指针或引用调用时,仍然会调用基类的虚函数,而不是派生类的新函数。
(4) 不能成为虚函数的特殊函数,某些特殊的成员函数不能被声明为虚函数,包括:
- 构造函数:构造函数负责初始化对象,在对象创建时自动调用,不能通过指针或引用来调用,因此不能是虚函数。
- 静态成员函数:静态成员函数属于类而不属于对象,不能访问非静态的成员变量和成员函数,因此不能是虚函数。
- 内联函数:内联函数在编译时展开,不会通过函数调用的方式执行,因此不能是虚函数。
- 友元函数:友元函数不是类的成员函数,因此不能是虚函数。
下面是一个示例,演示了虚函数的工作原理:
class Base {
public:
virtual void foo() {
std::cout << "Base::foo()" << std::endl;
}
void bar() {
std::cout << "Base::bar()" << std::endl;
}
};
class Derived : public Base {
public:
void foo() override {
std::cout << "Derived::foo()" << std::endl;
}
void bar() {
std::cout << "Derived::bar()" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->foo(); // 输出 "Derived::foo()",动态联编
ptr->bar(); // 输出 "Base::bar()",静态联编
delete ptr;
return 0;
}
在这个例子中,Base
类有一个虚函数foo
和一个非虚函数bar
。Derived
类继承自Base
类,并重载了foo
和bar
函数。在main
函数中,创建了一个Derived
对象,但是通过Base
的指针ptr
来管理它。
当调用ptr->foo()
时,会使用动态联编,根据实际对象的类型(Derived
)来调用Derived::foo()
。这是因为foo
是一个虚函数,编译器通过虚函数表来确定实际调用的函数。
当调用ptr->bar()
时,会使用静态联编,调用Base::bar()
。这是因为bar
不是虚函数,在编译时就确定了调用的目标。
1.5 抽象基类ABC
抽象基类(Abstract Base Class,ABC)是一种特殊的类,它被设计为其他类的基类,但不能直接实例化。在C++中,抽象基类至少包含一个纯虚函数(pure virtual function)。抽象基类主要用于定义一个通用的接口,供派生类继承和实现。
(1) 纯虚函数,是一种特殊的虚函数,在函数声明后加上 “= 0”,表示该函数没有实现,必须由派生类提供实现。例如:
class Shape {
public:
virtual double getArea() = 0;
};
在这个例子中,getArea
是一个纯虚函数,没有函数体。包含纯虚函数的类被称为抽象基类,不能直接实例化。
(2) 使用方法,抽象基类主要用于定义一个通用的接口,供派生类继承和实现。派生类必须实现所有的纯虚函数,才能被实例化。如果派生类没有实现所有的纯虚函数,那么派生类也会成为抽象基类。
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
double getArea() override {
return 3.14159 * radius * radius;
}
private:
double radius;
};
在这个例子中,Circle
类继承自抽象基类Shape
,并实现了纯虚函数getArea
。因此,Circle
类可以被实例化。
抽象基类的使用存在一些限制:
- 抽象基类不能直接实例化,只能作为其他类的基类。
- 抽象基类的构造函数可以被调用,但只能在派生类的构造函数中通过初始化列表调用。
- 如果一个类从抽象基类派生,那么它必须实现所有的纯虚函数,否则该类也会成为抽象基类。
- 抽象基类的析构函数通常应该是虚函数,以确保在销毁派生类对象时,派生类的析构函数被正确调用。
下面是一个完整的示例,演示了抽象基类的使用:
class Shape {
public:
virtual double getArea() = 0;
virtual double getPerimeter() = 0;
virtual ~Shape() {}
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
double getArea() override {
return 3.14159 * radius * radius;
}
double getPerimeter() override {
return 2 * 3.14159 * radius;
}
private:
double radius;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() override {
return width * height;
}
double getPerimeter() override {
return 2 * (width + height);
}
private:
double width;
double height;
};
int main() {
Shape* shapes[2];
shapes[0] = new Circle(2.0);
shapes[1] = new Rectangle(3.0, 4.0);
for (int i = 0; i < 2; ++i) {
std::cout << "Area: " << shapes[i]->getArea() << std::endl;
std::cout << "Perimeter: " << shapes[i]->getPerimeter() << std::endl;
delete shapes[i];
}
return 0;
}
在这个例子中,Shape
是一个抽象基类,包含两个纯虚函数getArea
和getPerimeter
。Circle
和Rectangle
类继承自Shape
,并实现了这两个纯虚函数。
在main
函数中,我们创建了一个Shape
指针数组,并分别存储了Circle
和Rectangle
对象的指针。通过基类指针,可以调用派生类实现的虚函数,实现多态。
最后,通过基类指针删除派生类对象,确保正确的析构函数被调用。
2. 代码重用
2.1 接口和实现
在C++中,接口(interface)和实现(implementation)是面向对象设计中的两个重要概念。接口定义了类的公共行为,而实现则是类的内部工作方式,C++通过类的继承和组合来实现接口和实现的分离。
(1) is-a关系(继承),表示一个类是另一个类的一种特殊类型。在C++中,is-a关系通过公有继承(public inheritance)来实现。派生类继承了基类的接口和实现,因此派生类对象可以被视为基类对象的一种特殊形式。
class Animal {
public:
virtual void eat() {
std::cout << "Animal is eating." << std::endl;
}
};
class Cat : public Animal {
public:
void eat() override {
std::cout << "Cat is eating." << std::endl;
}
void meow() {
std::cout << "Cat is meowing." << std::endl;
}
};
在这个例子中,Cat
类公有继承自Animal
类,表示"猫是一种动物"。Cat
类继承了Animal
类的eat
方法,并提供了自己的实现。此外,Cat
类还新增了一个meow
方法。
(2) has-a关系(组合),has-a关系表示一个类包含另一个类的对象作为其数据成员。在C++中,has-a关系通过组合(composition)或聚合(aggregation)来实现。组合表示被包含的对象与包含它的对象具有相同的生命周期,而聚合则表示被包含的对象可以独立于包含它的对象而存在。
class Engine {
public:
void start() {
std::cout << "Engine started." << std::endl;
}
};
class Car {
public:
void startEngine() {
engine.start();
}
private:
Engine engine;
};
在这个例子中,Car
类通过组合的方式包含了一个Engine
类的对象。这表示"汽车有一个引擎"。Car
类的startEngine
方法调用了Engine
对象的start
方法。
2.2 私有继承
在私有继承(private inheritance)中,基类的公有成员和保护成员在派生类中会成为私有成员。这意味着基类的接口不会成为派生类的接口的一部分,从而限制了类外对基类成员的访问。
在私有继承中,派生类可以在类内部使用基类的公有和保护成员,但在类外部,这些成员是不可访问的。为了在派生类内部访问基类的成员,需要使用基类的类名和作用域解析运算符(:😃。
class Base {
public:
void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived : private Base {
public:
void bar() {
Base::foo(); // 在派生类内部使用作用域解析运算符访问基类成员
}
};
int main() {
Derived d;
d.bar(); // 输出 "Base::foo()"
// d.foo(); // 错误,foo在派生类外部不可访问
return 0;
}
在某些情况下,私有继承可以被包含(composition)所替代。包含是指在一个类中将另一个类的对象作为数据成员。通过包含,可以在类内部访问被包含对象的公有成员,而不会暴露给类外部。
class Base {
public:
void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived {
public:
void bar() {
base.foo(); // 通过包含的对象访问基类成员
}
private:
Base base;
};
int main() {
Derived d;
d.bar(); // 输出 "Base::foo()"
return 0;
}
在私有继承中,如果想要暴露某些基类的成员给派生类外部使用,可以在派生类中使用using声明来重新定义这些成员的访问权限。
class Base {
public:
void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived : private Base {
public:
using Base::foo; // 使用using声明将foo暴露给派生类外部
};
int main() {
Derived d;
d.foo(); // 输出 "Base::foo()"
return 0;
}
访问权限:在公有继承中,基类的公有成员在派生类中保持公有,基类的保护成员在派生类中保持保护。而在私有继承中,基类的公有和保护成员在派生类中都会成为私有成员。
公有继承体现了is-a关系,派生类对象可以被视为基类对象。而私有继承不表示is-a关系,派生类对象不能被视为基类对象。公有继承主要用于实现多态和代码重用,派生类继承基类的接口和实现。而私有继承主要用于代码重用,派生类只继承基类的实现,而不继承接口。
2.3 虚基类(多重继承)
虚基类(virtual base class)是C++中用于解决多重继承下菱形继承(diamond inheritance)问题的一种机制。在菱形继承中,一个派生类从多个基类继承,而这些基类又共享一个公共的基类。这种情况下,派生类会继承多份公共基类的成员,导致二义性和冗余。虚基类通过在继承关系中引入虚拟继承,确保派生类只继承一份公共基类的成员。下面我将详细介绍虚基类的相关概念,包括多个子对象和一个虚子对象的情况,以及存在的二义性访问问题。
- 多个子对象的情况:
在普通的多重继承中,如果一个派生类从多个基类继承,而这些基类又共享一个公共的基类,那么派生类会包含多个公共基类的子对象。每个子对象都有自己的数据成员和成员函数,这可能导致冗余和不必要的内存开销。
class Base {
public:
int data;
};
class Derived1 : public Base {};
class Derived2 : public Base {};
class Diamond : public Derived1, public Derived2 {};
int main() {
Diamond d;
// d.data = 10; // 错误,访问二义性
d.Derived1::data = 10; // 显式指定访问 Derived1 的 data
d.Derived2::data = 20; // 显式指定访问 Derived2 的 data
return 0;
}
在这个例子中,Diamond
类从Derived1
和Derived2
继承,而它们都继承自Base
类。这导致Diamond
类包含两个Base
类的子对象,每个子对象都有自己的data
成员。当我们尝试直接访问d.data
时,会产生编译错误,因为存在访问二义性。我们需要显式指定访问哪个基类的data
成员。
- 一个虚子对象的情况:
通过在继承关系中引入虚拟继承,可以确保派生类只包含一个公共基类的子对象,称为虚子对象。虚拟继承使用virtual
关键字来声明基类。
class Base {
public:
int data;
};
class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};
class Diamond : public Derived1, public Derived2 {};
int main() {
Diamond d;
d.data = 10; // 正确,只有一个 Base 子对象
return 0;
}
在这个例子中,Derived1
和Derived2
通过虚拟继承自Base
类。这意味着Diamond
类只包含一个Base
类的虚子对象。现在,我们可以直接访问d.data
,不会产生二义性,因为只有一个Base
子对象。
- 二义性访问问题:
在菱形继承中,如果派生类从多个基类继承,而这些基类又共享一个公共的基类,那么在访问公共基类的成员时,可能会产生二义性。编译器无法确定应该访问哪个基类的成员。
class Base {
public:
void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived1 : public Base {
public:
void foo() {
std::cout << "Derived1::foo()" << std::endl;
}
};
class Derived2 : public Base {
public:
void foo() {
std::cout << "Derived2::foo()" << std::endl;
}
};
class Diamond : public Derived1, public Derived2 {};
int main() {
Diamond d;
// d.foo(); // 错误,访问二义性
d.Derived1::foo(); // 显式指定访问 Derived1 的 foo
d.Derived2::foo(); // 显式指定访问 Derived2 的 foo
return 0;
}
在这个例子中,Diamond
类从Derived1
和Derived2
继承,它们都重写了Base
类的foo
函数。当我们尝试调用d.foo()
时,会产生编译错误,因为存在访问二义性。编译器无法确定应该调用Derived1::foo()
还是Derived2::foo()
。我们需要显式指定调用哪个基类的foo
函数。
为了解决这个问题,可以使用虚拟继承:
class Base {
public:
virtual void foo() {
std::cout << "Base::foo()" << std::endl;
}
};
class Derived1 : virtual public Base {
public:
void foo() override {
std::cout << "Derived1::foo()" << std::endl;
}
};
class Derived2 : virtual public Base {
public:
void foo() override {
std::cout << "Derived2::foo()" << std::endl;
}
};
class Diamond : public Derived1, public Derived2 {
public:
void foo() override {
std::cout << "Diamond::foo()" << std::endl;
}
};
int main() {
Diamond d;
d.foo(); // 正确,调用 Diamond::foo()
return 0;
}
在这个例子中,Derived1
和Derived2
通过虚拟继承自Base
类,Diamond
类重写了foo
函数。现在,当我们调用d.foo()
时,编译器会根据虚函数的规则,调用Diamond::foo()
。虚拟继承确保了只有一个Base
子对象,消除了二义性。
总的来说,虚基类通过虚拟继承解决了多重继承下的菱形继承问题。它确保派生类只包含一个公共基类的子对象,避免了数据冗余和访问二义性。虚拟继承使用virtual
关键字来声明基类,派生类通过虚拟继承自基类。在设计类层次结构时,如果存在菱形继承的情况,使用虚基类可以简化继承关系,提高代码的可维护性。同时,也要注意虚拟继承会带来一定的内存和性能开销,应根据实际需求权衡使用。
3. 类模板
3.1 类模版定义和使用
参考文档:
- 模板 (C++) | Microsoft Learn
- typename | Microsoft Learn
- 类模板 | Microsoft Learn
C++类模板(class template)是一种通用的类定义,允许对类的成员类型进行参数化。通过使用模板,我们可以编写独立于具体数据类型的代码,提高代码的复用性和灵活性。
(1) 定义类模板,类模板的定义以关键字template
开头,后跟尖括号<>
,括号内包含一个或多个类型参数。类型参数可以是类类型、内置类型或其他模板类型。
template <typename T>
class MyClass {
public:
void setData(const T& value) {
data = value;
}
T getData() const {
return data;
}
private:
T data;
};
在这个例子中,MyClass
是一个类模板,T
是一个类型参数。类模板的成员函数和数据成员可以使用类型参数T
作为类型。
(2) 实例化类模板,要使用类模板,需要提供具体的类型参数来实例化模板。可以通过显式指定类型参数或使用自动类型推导来实例化类模板。
MyClass<int> intObj;
intObj.setData(42);
int value = intObj.getData();
MyClass<std::string> strObj;
strObj.setData("Hello");
std::string str = strObj.getData();
在这个例子中,MyClass<int>
和MyClass<std::string>
是MyClass
类模板的两个不同实例化,分别使用int
和std::string
作为类型参数。
(3) 模板参数推导,在某些情况下,编译器可以根据构造函数的参数类型自动推导类模板的类型参数。这称为模板参数推导。
MyClass intObj2(42); // 自动推导为 MyClass<int>
MyClass strObj2 = MyClass("Hello"); // 自动推导为 MyClass<const char*>
(4) 多个类型参数,类模板可以有多个类型参数,用逗号分隔。多个类型参数允许更灵活地定义类模板。
template <typename T1, typename T2>
class Pair {
public:
Pair(const T1& first, const T2& second)
: first(first), second(second) {}
T1 getFirst() const { return first; }
T2 getSecond() const { return second; }
private:
T1 first;
T2 second;
};
Pair<int, std::string> pair(42, "Hello");
(5) 默认模板参数,类模板可以为类型参数提供默认值,这样在实例化时可以省略对应的类型参数。
template <typename T1, typename T2 = std::string>
class MyPair {
// ...
};
MyPair<int> pair1(42, "Hello"); // 使用默认的 T2 类型 std::string
MyPair<int, char> pair2(42, 'A'); // 显式指定 T2 类型为 char
(6) 成员函数的定义,类模板的成员函数可以在类内部定义,也可以在类外部定义。在类外部定义成员函数时,需要使用template
关键字和类模板的类型参数列表。
template <typename T>
class MyClass {
public:
void setData(const T& value);
private:
T data;
};
template <typename T>
void MyClass<T>::setData(const T& value) {
data = value;
}
(7) 特化和偏特化,类模板可以有特化和偏特化版本,用于为特定的类型参数提供不同的实现。特化是为所有类型参数提供具体的类型,偏特化是为部分类型参数提供具体的类型或条件。
// 主模板
template <typename T>
class MyClass {
// ...
};
// 特化版本,针对 T 为 int 的情况
template <>
class MyClass<int> {
// ...
};
// 偏特化版本,针对 T 为指针类型的情况
template <typename T>
class MyClass<T*> {
// ...
};
(8) 类模板的继承,类模板可以继承自其他类或类模板。在继承时,需要指定基类的类型参数。
template <typename T>
class Base {
// ...
};
template <typename T>
class Derived : public Base<T> {
// ...
};
类模板是C++泛型编程的重要工具,通过将算法和数据结构与具体的数据类型解耦,提供了一种编写通用、可重用代码的方式。
3.2 递归使用(模版数组和非类型参数)
在C++中,模板可以递归地使用,即在模板定义中使用模板自身。这种递归使用模板的技术可以用于创建复杂的数据结构和算法。
(1) 递归使用模板,是指在模板定义中使用模板自身作为类型参数或默认参数。这种递归使用可以用于创建递归的数据结构,如链表、树等。下面是一个简单的递归模板示例:
template <typename T, typename U = void>
struct Node {
T data;
Node<T, U>* next;
};
template <typename T>
struct Node<T, void> {
T data;
Node<T>* next;
};
在这个例子中,Node
模板递归地使用自身作为next
指针的类型。当U
为void
时,递归结束,next
指针的类型变为Node<T>*
。
(2) 模板数组,是一种使用模板实现的多维数组。通过递归使用模板,可以创建任意维度的模板数组。下面是一个模板数组的示例:
template <typename T, size_t N>
class Array {
public:
using value_type = T;
using iterator = value_type*;
using const_iterator = const value_type*;
constexpr size_t size() const { return N; }
value_type& operator[](size_t index) { return data[index]; }
const value_type& operator[](size_t index) const { return data[index]; }
iterator begin() { return data; }
iterator end() { return data + N; }
const_iterator begin() const { return data; }
const_iterator end() const { return data + N; }
private:
value_type data[N];
};
template <typename T, size_t N, size_t... Dimensions>
class MultiDimensionalArray : public Array<MultiDimensionalArray<T, Dimensions...>, N> {};
template <typename T, size_t N>
class MultiDimensionalArray<T, N> : public Array<T, N> {};
在这个例子中,Array
模板表示一维数组,MultiDimensionalArray
模板通过递归使用Array
模板来创建多维数组。MultiDimensionalArray
模板接受一个类型参数T
和一个或多个非类型参数Dimensions
,表示数组的维度。
在模板定义中,除了类型参数,还可以使用非类型参数。非类型参数是在编译时确定的常量值,可以是整数、枚举、指针或引用。在上面的模板数组示例中,N
和Dimensions
就是非类型参数,表示数组的大小和维度。
模板数组的维度顺序与常规数组相反。在常规数组中,第一个维度是最外层,最后一个维度是最内层。而在模板数组中,第一个维度是最内层,最后一个维度是最外层。
using Matrix = MultiDimensionalArray<int, 3, 4>;
在这个例子中,Matrix
是一个3行4列的二维数组。与常规数组相比,行和列的顺序相反。
3.3 将模版用作参数
在C++中,模板可以使用另一个模板作为参数,这种情况称为模板模板参数(template template parameter)。通过使用模板模板参数,可以创建更加通用和灵活的模板代码。
(1) 模板模板参数的语法,类似于普通的模板参数,但在参数列表中使用关键字class
或typename
,后跟模板名和可选的默认参数。
template <template <typename> class Container>
class Wrapper {
public:
Container<int> data;
// ...
};
在这个例子中,Container
是一个模板模板参数,表示一个接受单个类型参数的模板。Wrapper
类内部可以使用Container
模板,并提供具体的类型参数。
(2) 使用模板模板参数时,需要提供一个符合要求的模板作为实参。这个模板必须与模板模板参数的声明匹配,即接受相同数量和类型的参数。
template <typename T>
class Vector {
// ...
};
Wrapper<Vector> wrapper;
在这个例子中,Vector
模板作为Wrapper
类的模板模板参数使用。Vector
模板接受单个类型参数,与Container
模板模板参数的声明匹配。
(3) 模板模板参数也可以有默认值,类似于普通的模板参数。默认值必须是一个与模板模板参数声明匹配的模板。
template <typename T, template <typename> class Container = std::vector>
class Wrapper {
public:
Container<T> data;
// ...
};
Wrapper<int> wrapper; // 使用默认的 std::vector 作为容器
在这个例子中,Container
模板模板参数有一个默认值std::vector
。如果在实例化Wrapper
类时没有提供容器模板,将使用默认的std::vector
。
(4) 模板模板参数也可以进行特化,针对特定的模板提供不同的实现。
template <template <typename> class Container>
class Wrapper {
// 通用实现
};
template <>
class Wrapper<std::vector> {
// std::vector 的特化实现
};
在这个例子中,Wrapper
类有一个通用实现和一个针对std::vector
的特化实现。当使用std::vector
作为容器模板时,将使用特化版本。
模板模板参数有一些限制,需要注意:
- 模板模板参数只能接受类模板,不能接受函数模板或别名模板。
- 模板模板参数的参数列表必须与实参模板的参数列表匹配,包括参数数量、类型和默认参数。
- 模板模板参数不能推导模板参数的类型,必须显式提供。
3.4 模板类和友元
当涉及到模版类时,友元函数和友元类可以用来访问模版类的私有和保护成员。
(1) 非模版友元,是指普通的函数或类,它们被声明为模版类的友元。这种友元可以访问模版类的所有实例的私有和保护成员。
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
friend void printData(const MyClass<T>& obj);
};
void printData(const MyClass<int>& obj) {
std::cout << "Data: " << obj.data << std::endl;
}
int main() {
MyClass<int> obj(42);
printData(obj);
return 0;
}
在这个例子中,printData
函数是MyClass
的非模版友元。它可以访问MyClass<int>
实例的私有成员data
。
(2) 模版友元函数,需要在模版类中使用friend
关键字,并在函数前面加上template
关键字。
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
template <typename U>
friend void printData(const MyClass<U>& obj);
};
template <typename U>
void printData(const MyClass<U>& obj) {
std::cout << "Data: " << obj.data << std::endl;
}
在这个例子中,在MyClass
模版类中声明了一个模版友元函数printData
。这个函数可以访问MyClass
的私有成员data
,并且可以接受任意类型的MyClass
对象作为参数。
模版友元函数的定义通常放在模版类的外部。它的定义需要以template
关键字开头,并指定模版参数。
template <typename T>
class MyClass {
private:
T data;
public:
MyClass(T val) : data(val) {}
template <typename U>
friend void printData(const MyClass<U>& obj);
};
template <typename U>
void printData(const MyClass<U>& obj) {
std::cout << "Data: " << obj.data << std::endl;
}
在这个例子中,printData
函数的定义放在了MyClass
模版类的外部。它以template <typename U>
开头,表示这是一个模版函数,并接受一个const MyClass<U>&
类型的参数。
调用模版友元函数与调用普通函数类似,只需要提供模版类的实例作为参数即可。
MyClass<int> obj(42);
printData(obj);
在这个例子中,创建了一个MyClass<int>
类型的对象obj
,并将其传递给printData
函数。printData
函数可以访问obj
的私有成员data
,并将其打印出来。
3.5 模版别名
在C++11中引入了模版别名,它提供了一种更简洁、更易读的方式来创建模版的别名。模版别名使用using
关键字来定义。
(1) 为模版类型定义别名,可以使用模版别名为模版类型定义一个新的名字。这在需要使用长模版类型名时特别有用。
template <typename T>
using Vec = std::vector<T, std::allocator<T>>;
Vec<int> vec = {1, 2, 3, 4, 5};
在这个例子中,使用using
关键字定义了一个模版别名Vec
,它代表了std::vector<T, std::allocator<T>>
类型。现在可以直接使用Vec<int>
来声明一个整型的vector,而不需要写出完整的类型名。
(2) 为模版定义别名,也可以使用模版别名为整个模版定义一个新的名字。这在需要对现有模版进行简单修改时非常有用。
template <typename T, typename U>
struct Pair {
T first;
U second;
};
template <typename T>
using IntPair = Pair<int, T>;
IntPair<std::string> pair = {42, "Hello"};
在这个例子中,首先定义了一个名为Pair
的模版结构体,它包含两个类型为T
和U
的成员变量。然后,使用using
关键字定义了一个模版别名IntPair
,它表示Pair<int, T>
类型。现在可以直接使用IntPair<std::string>
来声明一个包含整型和字符串的Pair
对象。
(3) 简化复杂的模版类型,模版别名可以用来简化复杂的模版类型,提高代码的可读性。
template <typename T, typename U, typename V>
using Triple = std::tuple<T, U, V>;
Triple<int, std::string, bool> triple = {42, "Hello", true};
在这个例子中,使用using
关键字定义了一个模版别名Triple
,它代表了std::tuple<T, U, V>
类型。现在可以直接使用Triple<int, std::string, bool>
来声明一个包含整型、字符串和布尔值的tuple
对象,而不需要写出完整的std::tuple
类型。
4. 特殊用法
4.1 嵌套类
参考文档:嵌套类声明 | Microsoft Learn
C++中的嵌套类是指在另一个类的定义中定义的类。嵌套类也称为内部类,它提供了一种将相关类组织在一起的方式,并可以访问外部类的成员。
(1) 嵌套类的访问权限,嵌套类可以在外部类的public
、protected
或private
部分中声明。嵌套类的访问权限决定了它对外部类和其他类的可见性。
public
嵌套类:可以被外部类和其他类访问,只有这个位置可以被外部空间直接访问。protected
嵌套类:可以被外部类及其派生类访问。private
嵌套类:只能被外部类访问。
class OuterClass {
public:
class PublicNestedClass {};
protected:
class ProtectedNestedClass {};
private:
class PrivateNestedClass {};
};
(2) 嵌套类的作用域,嵌套类位于外部类的作用域内,因此它可以访问外部类的所有成员,包括private
成员。要在外部类外访问嵌套类,需要使用外部类的作用域解析运算符::
。
OuterClass::PublicNestedClass publicObj;
(3) 嵌套类成员的访问权限,嵌套类的成员也可以有自己的访问权限,与普通类的成员类似。
- 嵌套类的
public
成员可以被外部类和其他具有访问权限的类访问。 protected
成员可以被外部类及其派生类访问。private
成员只能被嵌套类自身访问。
class OuterClass {
public:
class NestedClass {
public:
void publicMethod() {}
protected:
void protectedMethod() {}
private:
void privateMethod() {}
};
};
OuterClass::NestedClass nestedObj;
nestedObj.publicMethod(); // 可以访问
// nestedObj.protectedMethod(); // 错误,不能访问 protected 成员
// nestedObj.privateMethod(); // 错误,不能访问 private 成员
(4) 嵌套类的友元函数,嵌套类可以将外部类的成员函数声明为友元函数,以允许这些函数访问嵌套类的私有成员。
class OuterClass {
public:
void outerMethod() {
NestedClass nestedObj;
nestedObj.privateMethod(); // 可以访问,因为 outerMethod 是友元函数
}
private:
class NestedClass {
private:
void privateMethod() {}
friend void OuterClass::outerMethod();
};
};
4.2 异常机制
参考文档:
- MSVC 中的异常处理 | Microsoft Learn
- C++ 异常处理_w3cschool
C++的异常机制提供了一种处理程序运行时错误的方式。它允许在发生错误时抛出异常,并在适当的位置捕获和处理这些异常。
(1) 基础使用方式:
- 使用
try
块来包含可能抛出异常的代码。 - 使用
catch
块来捕获和处理特定类型的异常。 - 使用
throw
关键字来抛出异常。
try {
// 可能抛出异常的代码
if (error) {
throw std::runtime_error("An error occurred");
}
} catch (const std::exception& e) {
// 处理异常
std::cout << "Exception caught: " << e.what() << std::endl;
}
(2)noexcept
异常规范,noexcept`是C++11引入的一个关键字,用于指定函数是否会抛出异常。
- 如果函数声明为
noexcept
,则表示它不会抛出任何异常。 - 如果
noexcept
函数抛出异常,程序将调用std::terminate()
函数终止。
void func() noexcept {
// 不会抛出异常的代码
}
(3) 栈解退和栈对象自动析构, 当异常被抛出时,程序会沿着调用栈向上搜索匹配的catch
块。在此过程中,栈上的对象会被自动析构。
- 构造完成的对象在栈解退过程中会调用析构函数。
- 构造未完成的对象不会调用析构函数。
void func() {
MyClass obj1; // 构造完成
MyClass* ptr = new MyClass(); // 动态分配,不在栈上
MyClass obj2; // 构造完成
throw std::exception(); // 抛出异常
} // obj2和obj1的析构函数会被调用,但ptr指向的对象不会被自动释放
(4) 异常基类和常见派生对象,C++标准库定义了一个异常基类std::exception
,以及几个常见的派生异常类。
std::logic_error
:逻辑错误,如无效参数等。std::runtime_error
:运行时错误,如除以零等。std::bad_alloc
:内存分配失败。std::out_of_range
:超出范围访问。
try {
// ...
} catch (const std::logic_error& e) {
// 处理逻辑错误
} catch (const std::runtime_error& e) {
// 处理运行时错误
} catch (const std::exception& e) {
// 处理其他标准库异常
}
(5) 任意异常捕获,可以使用...
来捕获任意类型的异常。这种方式应谨慎使用,因为它可能会捕获到意料之外的异常。
try {
// ...
} catch (...) {
// 处理任意异常
}
(6) 意外异常和未捕获异常的默认行为和自定义方式:
- 如果抛出的异常没有被捕获,程序将调用
std::terminate()
函数终止。 - 可以使用
std::set_terminate()
函数来自定义未捕获异常的处理方式。
void custom_terminate() {
std::cout << "Uncaught exception, terminating..." << std::endl;
std::abort();
}
int main() {
std::set_terminate(custom_terminate);
// ...
}。
4.3 异常安全代码
在C++中实现异常安全性设计需要考虑多个方面。
(1) 异常安全的函数设计:
- 函数要么完全成功,要么将程序状态恢复到调用前的状态。这被称为"强异常安全保证"。
- 如果无法提供强异常安全保证,应该提供"基本异常安全保证",即确保程序状态保持一致,不会泄漏资源或损坏数据。
- 对于不抛出异常的函数,可以使用
noexcept
关键字进行标记,以便编译器进行优化。
(2) RAII(Resource Acquisition Is Initialization)技术:
- 使用RAII技术来管理资源,如内存、文件句柄等。
- 将资源封装在类中,在构造函数中获取资源,在析构函数中释放资源。
- 当异常发生时,RAII对象会在栈解退过程中自动释放资源,避免资源泄漏。
class FileHandle {
public:
FileHandle(const std::string& filename) : file(std::fopen(filename.c_str(), "r")) {
if (!file) {
throw std::runtime_error("Failed to open file");
}
}
~FileHandle() {
if (file) {
std::fclose(file);
}
}
// 其他成员函数...
private:
std::FILE* file;
};
(3) 异常中立的代码:
- 对于不抛出异常的代码,如标准库函数,需要进行适当的错误检查和处理。
- 使用错误码、返回值等方式来表示错误状态,而不是抛出异常。
- 在调用这些函数时,需要检查返回值并进行相应的处理。
int result = std::printf("Hello, %s!\n", name);
if (result < 0) {
// 处理错误
}
(4) 异常转换:
- 当在异常代码和非异常代码之间进行交互时,可能需要进行异常转换。
- 将异常转换为错误码或其他形式的错误表示,以便与非异常代码兼容。
- 在适当的位置捕获异常,并将其转换为相应的错误表示。
try {
// 可能抛出异常的代码
} catch (const std::exception& e) {
// 将异常转换为错误码或其他形式的错误表示
return ERROR_CODE;
}
(5) 异常规范和异常处理:
- 对于可能抛出异常的函数,使用异常规范(如
noexcept(false)
)来表明其异常行为。 - 在调用这些函数时,使用
try-catch
块来捕获和处理异常。 - 对于未捕获的异常,可以使用
std::set_terminate()
函数来指定自定义的终止处理程序。
4.3 RTTI(运行期类型)
C++的RTTI(Run-Time Type Information,运行时类型信息)机制提供了在运行时确定对象类型的能力。RTTI主要包括dynamic_cast
运算符、typeid
运算符以及type_info
类。
(1)dynamic_cast
用于在运行时执行多态类型的转换。它主要用于将基类指针或引用转换为派生类指针或引用。
- 如果转换成功,
dynamic_cast
返回一个指向派生类对象的指针或引用。 - 如果转换失败,对于指针类型,
dynamic_cast
返回nullptr
;对于引用类型,dynamic_cast
抛出std::bad_cast
异常。
class Base { virtual void dummy() {} };
class Derived : public Base {};
Base* ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(ptr);
if (derived_ptr) {
// 转换成功
} else {
// 转换失败
}
(2) typeid
运算符用于获取表达式的类型信息。它返回一个const std::type_info&
类型的对象,该对象包含有关类型的信息。
- 对于多态类型,
typeid
返回对象的实际类型信息。 - 对于非多态类型,
typeid
返回静态类型信息。
class Base { virtual void dummy() {} };
class Derived : public Base {};
Base* ptr = new Derived();
const std::type_info& info = typeid(*ptr);
std::cout << "Type name: " << info.name() << std::endl;
(3) type_info
类提供了有关类型的信息。它包含以下成员函数:
const char* name() const
:返回类型的名称。bool operator==(const type_info& rhs) const
:比较两个type_info
对象是否相等。bool before(const type_info& rhs) const
:比较两个type_info
对象的顺序。
class Base { virtual void dummy() {} };
class Derived : public Base {};
Base* ptr1 = new Base();
Base* ptr2 = new Derived();
const std::type_info& info1 = typeid(*ptr1);
const std::type_info& info2 = typeid(*ptr2);
if (info1 == info2) {
std::cout << "Types are the same" << std::endl;
} else {
std::cout << "Types are different" << std::endl;
}
需要注意的是,使用RTTI机制会带来一些运行时开销,因为它需要在运行时进行类型检查和转换。此外,RTTI只适用于包含虚函数的类层次结构。
4.5 类型转换符
在C++中,有四个显式类型转换运算符:dynamic_cast
、const_cast
、static_cast
和reinterpret_cast
。
(1) dynamic_cast
:
- 用于在运行时执行多态类型的转换。
- 主要用于将基类指针或引用转换为派生类指针或引用。
- 如果转换成功,返回一个指向派生类对象的指针或引用;如果转换失败,对于指针类型,返回
nullptr
;对于引用类型,抛出std::bad_cast
异常。 - 只能用于包含虚函数的类层次结构。
class Base { virtual void dummy() {} };
class Derived : public Base {};
Base* ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(ptr);
(2) const_cast
:
- 用于移除或添加对象的
const
或volatile
限定符。 - 常用于将
const
指针或引用转换为非const
指针或引用。 - 不能用于去除静态类型的
const
性。
const int x = 10;
int* ptr = const_cast<int*>(&x);
*ptr = 20; // 未定义行为
(3) static_cast
:
- 用于执行隐式类型转换,如算术类型之间的转换、指针之间的转换等。
- 不执行运行时类型检查。
- 不能用于在不相关的类型之间进行转换。
int x = 10;
double y = static_cast<double>(x);
Base* ptr = new Derived();
Derived* derived_ptr = static_cast<Derived*>(ptr);
(4) reinterpret_cast
:
- 用于执行低级别的类型转换,如将指针转换为整数、将一种指针类型转换为另一种指针类型等。
- 不执行任何类型检查或转换,仅重新解释内存中的比特位。
- 使用时需要谨慎,因为它可能导致未定义行为。
int x = 10;
int* ptr = &x;
uintptr_t address = reinterpret_cast<uintptr_t>(ptr);
总而言之,dynamic_cast
用于多态类型的运行时转换,const_cast
用于移除或添加const
或volatile
限定符,static_cast
用于执行隐式类型转换,reinterpret_cast
用于执行低级别的类型转换。