C++类 & 模板¶
成员访问¶
在C++中,使用类时需要为其成员配置访问等级权限。访问等级分为三级:public、protected、private。
-
public(公有)表示该成员可由该类的对象直接访问,通常用于向外部提供类的接口。 -
protected(保护)表示该成员在类的外部只能由该类的派生类(子类)访问,用于类的继承,允许派生类进行合理的扩展。 -
private(私有)表示该成员只能在类的内部访问,在类的外部(包括派生类)都是不可访问的,这通常用于隐藏类的内部数据和实现细节,防止外部直接访问和修改,以保证类的稳定性与安全性。
Class VS Struct
类成员的访问操作与结构体类似,但二者的区别在于类的成员具有严格的访问权限设置,而结构体的成员没有。使用类成员的访问权限等级进行类比,结构体成员的访问权限等级都是public。
定义类¶
多文件(.h&.cpp)¶
头文件(.h/.hpp)
-
用于定义类并存放它的接口
-
内容上包含:
-
函数(类方法)原型
-
变量(类成员变量)声明
-
类定义
-
类型定义
-
宏和常量
-
模板定义
-
Note
有关类模板的介绍,移步Class Templates | CS106L-TextBook
源文件(.cpp/.cc)
-
用于存放头文件中声明的类方法和类定义的实现
-
内容上包含:
-
函数(类方法)实现
-
可执行代码
-
简单来说,头文件用于定义类的结构,源文件用于实现类的定义细节。
以上图片来源于CS106L的课程幻灯片。
设计类¶
一个类的大致包含以下几个模块:
-
构造函数(Constructor)
-
私有成员变量/函数
-
公有成员变量/函数
-
析构函数(Destructor)
构造函数(Constructor)¶
构造器/构造函数用于在创建类的新对象时初始化该对象的状态。
假设我们想实现一个Student类,我们可以这样定义:
Header File:
class Student {
private:
std::string name;
long int id;
int age;
public:
// constructor
Student(std::string name, long int id, int age);
// memeber methods
std::string getName();
long int getID();
int getAge();
};
#include "Student.h"
#include <string>
// implement constructor
Student::Student(std::string name, long int id, int age) {
/// keyword 'this' is neccessary in here in order to avoid naming conflict
this->name = name;
this->id = id;
this->age = age;
}
// implemented members
std::string Student::getName() {
// return name; /// alternative
return this->name;
}
long int Stdudent::getID() {
return this->id;
}
int Student::getAge() {
return this->age;
}
// implemented setter functions
void Student::setName(std::string name) {
// use 'this' and '=' here!
this->name = name;
}
void Student::setID(long int id) {
this->id = id;
}
void Student::setAge(int age) {
this->age = age;
}
注意在源文件中实现类方法时需要在函数名前添加它的namespace(命名空间),通常是类的名称。
自C++11起,构造器支持使用列表初始化:
Student::Student(std::string name, long int id, int age) name{name}, id{id}, age{age} {}
对于构造函数,我们还能定义其默认构造方式:
// defualt constructor
Student::Student() name{"virtualguard"}, id{100100101}, age{18} {}
// overload by parameterized constructor
Student::Student(std::string name, long int id, int age) name{name}, id{id}, age{age} {}
析构函数(Destructor)¶
在C++中,析构函数用于在对象生命周期结束时执行清理工作,是对象生命周期中的重要部分。
析构函数不需要显示调用,它会在对象离开其作用范围时自动调用。
析构函数的语法如下:
Student::~Student() {
// free/deallocate any data here
delete [] some_array; // for illastration
}
类继承¶
下图展示的是stream的继承关系。
以上图片来源于CS106L的课程幻灯片。
针对继承在面向对象编程中的作用与使用思想,课程CS61A | UC Berkeley有详尽的讲解,这里直接看示例。
以几何图形的定义与性质为例:
Header File:
class Shape {
public:
// virtual function
virtual double area() const = 0;
};
class Circle: public Shape {
public:
Circle(double radius): _radius{radius} {};
double area() const {
return M_PI * _radius * radius;
}
private:
// private members are marked by '_'
double _radius;
};
class Rectangle: public Shape {
public:
Rectangle(double width, double height): _height{height}, _width{width} {};
double area() const {
return _width * _height;
}
private:
double _width, _height;
}
杂项¶
类型别名¶
允许用户为现有类型创建别名。一般有两种方式:
typedef¶
// typedef [origin] [aliasing]
typedef int Integer;
typedef double Real;
using¶
使用using关键字进行类型别名定义是C++11引入的一个特性。第一节的学习中我们就有用到它。
// using [aliasing] = [origin]
using String = std::string;
using quadratic = std::pair<bool, std::pair<double, double>>;
模板¶
模板类¶
模板类是一种基于多种类型进行参数化定义的类,具有通用类型组成的成员变量构成。
模板类用于泛化类跨数据类型,使用模板类能够极大地提升代码的复用性。
假设我们想要实现多个自定义容器,但它们在某些功能上具有通用性,我们就可以使用模板类:
template <typename T>
class Container {
public:
Container(T val);
T getValue();
private:
T value;
};
也可同时传入多个类型名作为模板参数:
template <typename T, typename U>
Note
在STL中,所有的容器都是类。
模板函数¶
模板函数与模板类的声明类似,即使用template关键字进行声明:
// swap template function
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
也可为模板参数设置默认类型:
template <typename Type=int>
Type customMin(Type a, Type b) {
return a < b ? a : b;
}
int main() {
std::cout << customMin<int>(101, 5) << std::endl;
return 0;
}
多参数:
template <typename T, typename U>
auto smartMin(T a, U b) {
return a < b ? a : b;
}
int main() {
std::cout << smartMin(101, 5.5) << std::endl;
return 0;
}
针对上面在头文件中定义的模板类的结构,我们可以在源文件中通过模板函数来完成其成员函数的实现:
template <class T>
Container<T>::Container(T val) {
this->value = val;
}
template <typename T>
T Container<T>::getValue() {
retutn value;
}
在绝大多数情况下,模板参数中的class和typename可以互换。
Note
需注意若上述模板函数为以下形式,即未在类的命名空间后添加模板类的参数类型,模板参数将不会传递:
template <class T>
Container::Container(T val) {
this->value = val;
}
template <typename T>
T Container::getValue() {
retutn value;
}
注意事项¶
在创建模板类时,需要在定义模板类结构的.h文件中包含对应的.cpp实现:
template <typename T>
class Container {
public:
Container(T val);
T getValue();
private:
T value;
};
// must do this!
#include "Container.cpp"
模板元编程¶
模板元编程(TMP, Template Metaprogramming)是C++中的一种编程范式,它允许在编译时进行计算和决策,从而提高程序的效率和可维护性。
合理使用模板可以提高效率。
template<unsigned n>
struct Factorial {
enum { value = n * Factorial<n - 1>::value };
}
template<> // template class "specialization"
struct Factorial {
enum { value = 1 };
}
int main() {
std::cout << Factorial<10> << '\n'; // 3628800
}
上面是一个阶乘程序,但Factorial<10>的结果会在编译时直接计算,而不是通常情况下的运行时。这能提高程序在运行时的效率,同时也为类似递归的资源密集型操作提供了接口模板。
constexpr¶
在C++中,也可使用constexpr声明表达式在编译时运行。constexpr关键字用于指定一个常量表达式。
constexpr double fib(int n) {
if (n == 1) { return 1; }
return fib(n - 1) * n;
}
Note
-
常量表达式必须在声明时立即进行初始化,以便于编译器在编译时直接运行它
-
传递给常量表达式的参数也应当是常量/常量表达式
变量也可以使用constexpr修饰。
const vs constexpr¶
在实际工程中,const与constexpr都可以用于“不可修改”的语义,但二者的关注点不同:
-
const:强调运行期只读(不可修改) -
constexpr:强调编译期常量表达式(也不可修改)
以播放器解码流程中的分块读取为例:
constexpr ma_uint64 kChunkFrames = 4096;
std::vector<float> chunk(kChunkFrames * decoded.channels, 0.0f);
这里使用constexpr的原因是:kChunkFrames是一个固定参数,不依赖运行时输入,且语义上希望明确它是“编译期确定的常量”。
如果写成:
const ma_uint64 kChunkFrames = 4096;
在这个函数里通常也能正常工作,但它表达的是“不可改”,而不如constexpr直接强调“编译期常量”的意图。
再看一个更适合const的值:
const size_t copySamples = static_cast<size_t>(readFrames * decoded.channels);
copySamples依赖readFrames(运行时数据),它不是编译期常量;此时使用const表示“本次作用域内不再修改”就足够了。
可记为:
-
固定配置参数、编译期可确定:优先
constexpr -
运行期计算得到但希望只读:使用
const
真正拉开差距的场景¶
很多时候二者看起来“都能用”,但在需要编译期常量的上下文里,差距会非常明显。
- 场景一:用于模板参数(例如
std::array长度)
constexpr int kCompileSize = 4096;
std::array<float, kCompileSize> a;
上例中kCompileSize是编译期常量表达式,因此可作为模板参数。
如果改成依赖运行时数据:
int runtimeSize = decoded.channels * 1024;
const int kRuntimeSize = runtimeSize;
// std::array<float, kRuntimeSize> b; // 编译错误:不是常量表达式
虽然kRuntimeSize被const修饰,不可修改,但它的值来自运行时,因此不能用于要求编译期常量的场景。
- 场景二:初始化约束不同
const int x = runtimeSize; // 合法:运行时只读变量
// constexpr int y = runtimeSize; // 非法:constexpr要求编译期可求值
这里体现出核心差异:
-
const:运行时计算得到的值后续不能修改 -
constexpr:编译阶段就必须可确定
因此,若希望变量不仅不可改,还能参与编译期计算(模板参数、数组维度、if constexpr分支等),应优先选择constexpr。
常量接口¶
现有一个用户自定义类Student:
class Student {
public:
Student(String name, long int id, int age);
void setName(String name);
String getName();
long int getID();
int getAge();
private:
using String = std::string;
String _name;
long int _id;
int _age;
}
同时有如下函数:
std::string getAgeInfo(const Student& s) {
return s.getName() + "is" + std::to_string(s.getAge()) + "years old."
}
/// compile error
那么在编译时就会出现错误,原因如下:
-
在
getAgeInfo()中,我们使用const修饰了对象s,以声明其不会被修改 -
然而,编译器并不知道
getName()和getAge()是否会修改对象s -
根据类的特性,成员函数能够访问并修改成员变量
解决的方法也很简单,在成员函数声明(头文件)和实现(源文件)的地方中分别在各个函数的参数列表后添加const修饰即可:
class Student {
public:
Student(String name, long int id, int age);
void setName(String name);
String getName() const;
long int getID() const;
int getAge() const;
private:
using String = std::string;
String _name;
long int _id;
int _age;
};
#include <string>
#include "Student.h"
std::string Student::getName() const {
return this->_name;
}
long int Student::getID() const {
return this->_id;
}
int Student::getAge() const {
return this->_age;
}
被const所修饰的函数就是所谓的常量接口,即不会改变类的对象/示例属性的函数。
具有const属性的对象只能与常量接口进行交互。
应用实例¶
现有一个用户自定义类IntArray:
class IntArray {
public:
IntArray(size_t size);
~IntArray();
int& at(size_t index);
int size();
private:
int *_array;
size_t _size;
}
IntArray::IntArray(size_t size) : _size(size), _array(new int[size]) {}
IntArray::~IntArray() {
delete [] _array;
}
int& at(size_t index) {
return _array[index];
}
// overload const interface
int& at(size_t index) const {
return _array[index];
}
int size() {
return this->_size;
}
在main函数调用该类:
#include "IntArray.h"
#include <iostream>
static void printElement(const IntArray& arr, size_t index) {
std::cout << arr.at(index) << std::endl;
}
int main() {
IntArray arr = IntArray(5);
int& secondVal = arr.at(1);
secondVal = 101;
printElement(arr, 1);
return 0;
}
const_cast¶
在IntArray中,现有一个成员函数findItem,用于查找元素:
int& findItem(int value) {
for (auto& elem : arr) {
if (elem == value) { return elem; }
}
throw std::out_of_range("value not found");
}
// overload const interface
const int& findItem(int value) const {
for (auto& elem : arr) {
if (elem == value) { return elem; }
}
throw std::out_of_range("value not found");
}
对于常量接口的重写版本,我们可以使用const_cast将其简化为一行:
int& findItem(int value) {
for (auto& elem : arr) {
if (elem == value) { return elem; }
}
throw std::out_of_range("value not found");
}
// use const_cast
const int& findItem(int value) const {
return const_cast<IntArray&>(*this).findItem(value);
}
-
const_cast用于将当前的const引用转换为非const引用 -
<IntArray&>表示转换的非const引用目标 -
(*this)表示对指针this进行解引用操作,以将其转换为非const引用 -
findItem(value)表示调用成员函数
总结起来,使用const_cast通常分为三步:
-
设置指向一个非常量对象的状态
-
调用该函数的非常量版本
-
将函数调用中非常量类型的返回值转换为常量版本


