在介绍 C++11 之前,我们先回顾一下 C++98和C++03。C++98 作为 C++ 的第一个国际标准,奠定了这门语言的基础结构和核心特性,比如类、继承、模板、异常处理等。这些特性使得 C++ 成为一门强大的、面向对象的编程语言,广泛应用于系统/应用软件、游戏开发、实时系统等领域。C++03 则是对 C++98 进行了修订,主要解决标准的疑义和错误,没有引入新特性。
然而,随着软件开发的不断进化,C++98和C++03 在表达能力、编程便利性和性能方面显示出了局限性。比如:对并发编程的支持不够强大,模板编程有时显得过于复杂,资源管理(尤其是内存管理)易于出错等。这些局限性促使了 C++11 标准的诞生,它被视为 C++ 的一次重大更新,于 2011 年正式发布。这次更新标志着 C++ 进入现代化的重要一步,引入了许多新特性和改进,旨在使 C++ 更易于使用,更灵活,同时提高了代码的安全性和性能。
C++11 新增了很多新特性值得我们学习,包含如下:
核心语言增强
1. 自动类型推断 (auto)
auto 关键字让编译器能够自动推断变量的类型(通过变量初始化时的表达式来确定类型),简化了变量声明的语法。
语法:
auto variable_name = expression;
auto x = 5; // x 是 int
auto y = 3.14; // y 是 double
2. 获取表达式的类型(decltype)
语法
decltype(expression) variableName;
这里,expression 是你要查询类型的表达式,而 variableName 是使用该表达式类型声明的变量名称。
int x = 5;
decltype(x) y = x; // y 的类型是int
在这个例子中,decltype(x)将y的类型推导为x的类型,即int
。
结合 auto 使用:
auto x = 1; // x 的类型是int
decltype(x) y = x; // y 的类型也是int
用于复杂表达式
decltype
特别有用于表达式的类型不明显时:
3. 基于范围的 for 循环
C++11引入了基于范围的for循环(Range-based for loop),这是一个用于遍历序列(如数组、容器等)的语法糖。它简化了迭代序列中每个元素的代码书写方式,使代码更加简洁易读。
基本语法:
for (declaration : range) {
// 循环体
}
-
declaration:用于迭代序列中每个元素的变量声明。这个变量的类型可以是序列元素的类型,也可以是(auto)自动类型推导。
-
range:要迭代的序列,可以是数组、容器(如std::vector、std::list等)或任何支持begin()和end()方法的对象。
使用 auto 自动类型推导
#include <vector>
#include <iostream>
int main() {
std::vector<std::string> words = {"Hello", "World", "!"};
for (auto word : words) {
std::cout << word << ' ';
}
// 输出: Hello World !
return 0;
}
注意事项:
如果你需要在循环中修改序列中的元素,请使用引用&
来声明变量。
std::vector<int> vec = {1, 2, 3};
for (int& num : vec) {
num *= 2; // 修改元素值
}
如果不需要修改元素,并且元素类型较大时,考虑使用常量引用const &
来避免不必要的拷贝,提高效率。
for (const auto& word : words) {
std::cout << word << ' ';
}
基于范围的for循环是 C++11 中引入的一项便利特性,通过简化集合的遍历操作,它让代码更加简洁,增强了代码的可读性和易用性。
4. 统一初始化
C++11 引入了统一初始化(Uniform Initialization),这是一种使用花括号 {}
进行变量初始化的语法。它提供了一种一致的语法来初始化任何对象。
语法:
Type variable{value1, value2, ...};
基本类型的初始化:
int a{10};
double b{3.14};
聚合类型(如结构体和数组)的初始化:
struct Point {
int x, y;
};
Point p{10, 20};
int arr[]{1, 2, 3, 4, 5};
容器的初始化
#include <vector>
std::vector<int> v{1, 2, 3, 4, 5};
类对象的初始化
class MyClass {
public:
MyClass(int x, double y) : x_(x), y_(y) {}
private:
int x_;
double y_;
};
MyClass obj{5, 3.14};
5. 初始器列表(Initializer Lists)
初始器列表是C++11引入的一项特性,它进一步扩展了统一初始化的能力,特别是对于容器和自定义类对象的初始化。它允许构造函数接收一个由花括号{}
包围的元素列表,从而提供了一种简洁且强大的初始化方式。
包含必要头文件
要使用初始器列表,你需要包含<initializer_list>
头文件。
#include <initializer_list>
语法
初始器列表主要通过在类构造函数中使用std::initializer_list<T>
类型的参数来实现,其中T
是列表中元素的类型。
class ClassName {
public:
ClassName(std::initializer_list<T> list);
};
示例代码:
自定义类的初始器列表
假设有一个代表简单整数集合的类,我们希望能够在创建对象时直接用一组整数来初始化这个集合:
#include <initializer_list>
#include <iostream>
#include <vector>
class IntSet {
std::vector<int> elements;
public:
IntSet(std::initializer_list<int> list) : elements(list) {
std::cout << "Initialized with elements: ";
for (int elem : elements) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};
int main() {
IntSet mySet = {1, 2, 3, 4, 5};
return 0;
}
// 输出: Initialized with elements: 1 2 3 4 5
函数参数为初始器列表:
函数也可以接受std::initializer_list<T>
类型的参数,这在需要传递一组值时非常有用:
#include <initializer_list>
#include <iostream>
void print(std::initializer_list<int> vals) {
for (auto val : vals) {
std::cout << val << " ";
}
std::cout << std::endl;
}
int main() {
print({10, 20, 30, 40, 50});
return 0;
}
// 输出: 10 20 30 40 50
总结
初始器列表(Initializer Lists)
为C++11提供了一种强大的初始化机制,特别是在初始化需要一组值的对象时。通过使用初始器列表,可以极大地简化代码,提高可读性和可维护性。这一特性在自定义类、函数参数传递时尤为有用。
6. nullptr 关键字
在 C++11 中,nullptr
是一个特殊的字面量,用于表示空指针。它是对之前 C++ 版本中使用整数 0 或宏 NULL 来表示空指针的改进。nullptr
的引入提供了一种类型安全的方式来表示没有指向任何对象的指针。
定义及使用:
int* ptr = nullptr;
if (ptr == nullptr) {
// 检查 ptr 是否为空
}
为什么需要 nullptr?
-
类型安全:在 C++11 之前,NULL 通常被定义为 0,这意味着它实际上是一个整数。这可能导致类型混淆和错误,特别是在函数重载的情况下。
nullptr
明确地表示一个空指针,不会与整数混淆。 -
更好的语义:
nullptr
直观地表示指针为空,改善了代码的可读性和意图表达。
示例代码:
使用 nullptr 初始化指针:
int* ptr = nullptr; // ptr 是一个指向 int 的空指针
函数重载中 nullptr 的优势:
void func(int) {
std::cout << "func(int) called" << std::endl;
}
void func(int*) {
std::cout << "func(int*) called" << std::endl;
}
int main() {
func(0); // 调用 func(int)
func(nullptr); // 明确调用 func(int*)
return 0;
}
在这个例子中,使用 nullptr
可以明确地调用接受指针参数的重载函数版本,避免了潜在的歧义。
通过引入nullptr
,C++11 提高了代码的类型安全性和清晰度,明确区分了整数 0 和空指针的概念。
7. 长长整形(Long Long Int)
在 C++11 之前,整型的最大大小受限于long int,其大小至少为32位。为了支持更大的整数,C++11引入了long long int
和unsigned long long int
类型,保证至少64位的大小。
示例:
long long int bigNumber = 9223372036854775807LL; // LL 表示这是一个 long long 类型的字面量
unsigned long long int bigUnsignedNumber = 18446744073709551615ULL; // ULL 表示这是一个 unsigned long long 类型的字面量
8. 无符号字面量
无符号字面量就是用来表示无符号整数的字面量。在C++中,可以通过在整数后面添加U
或u
来创建无符号整型字面量。
注意:字面量(Literal)是指在源代码中直接表示其值的固定值的表示法。字面量可以是数字、字符、字符串或其他固定值。
示例:
unsigned int x = 123U; // U 表示无符号整数
9. 用户自定义字面量
C++11引入了用户自定义字面量(User-Defined Literals, UDL),允许开发者定义自己的字面量操作符,为字面量赋予新的含义。这通过定义一个以_
开头的字面量操作符函数实现。
语法:
return_type operator "" _customSuffix(const char*);
示例:
定义一个将字符串转换为复数的自定义字面量:
#include <iostream>
#include <complex>
// 定义自定义字面量 _i,用于创建复数
std::complex<double> operator"" _i(long double d) {
return std::complex<double>(0, d); // 第一个参数表示所构造复数的实部,第二个参数代表虚部。
}
int main() {
auto c = 3.14_i; // 使用自定义字面量创建一个复数,将会调用operator"" _i 函数
std::cout << "Real part: " << c.real() << ", Imaginary part: " << c.imag() << std::endl;
return 0;
}
在这个例子中,_i
是自定义的字面量操作符,它将跟随它的数字转换为一个复数。这使得代码更加直观和易于理解。
10. 强类型枚举 (enum class)
C++11 引入了强类型枚举,也称为作用域枚举(scoped enums),使用 enum class
关键字定义。与传统的枚举(unscoped enums)相比,强类型枚举具有更好的类型安全性,不会隐式地转换为整型,枚举值必须在作用域内访问,并且可以指定底层类型。
语法
enum class EnumName : UnderlyingType {
enumerator1,
enumerator2,
...
};
-
EnumName 是枚举类型的名称。
-
UnderlyingType 是用来表示枚举值的底层类型,通常是某种整型(如int、unsigned int、short等)。如果没有该字段,则底层类型默认为 int
强类型枚举定义
enum class Color : unsigned int {
Red,
Green,
Blue
};
enum class StatusCode : char {
Ok = 'O',
Error = 'E',
Unknown = 'U'
};
简单例子
#include <iostream>
// 传统枚举
enum Color { Red, Green, Blue };
// 强类型枚举 底层类型为 int
enum class StrongColor { Red, Green, Blue };
int main() {
Color c = Red; // 直接访问
// StrongColor sc = Red; // 错误:Red 不在作用域内
StrongColor sc = StrongColor::Red; // 正确:使用作用域访问
// int colorInt = sc; // 错误:不能隐式转换为整型
int colorInt = static_cast<int>(sc); // 正确:需要显式转换
std::cout << colorInt << std::endl; // 输出:0,假设 StrongColor::Red 底层对应的整数值为 0
return 0;
}
在这个例子中,强类型枚举 StrongColor 的使用增加了类型安全性,避免了与整型之间的隐式转换,并且强制使用枚举类名作为作用域来访问枚举值。这些特性有助于避免命名冲突和提高代码清晰度。
11. 常量表达式 (constexpr)
C++11 引入了 constexpr
关键字,用于定义常量表达式。这个关键字可以用于变量、函数和构造函数,允许在编译时进行计算,而不是运行时。这对于提高程序的性能非常有用,因为它允许在编译期间执行更多的计算,减少运行时的工作量。
语法
定义常量表达式变量:
// type 变量类型
constexpr type variable = value;
// 定义常量
constexpr int max_size = 100;
定义常量表达式函数:
// type 函数的返回值类型
constexpr type function_name(parameters) {
// 函数体
}
定义常量表达式函数:
constexpr int square(int x) {
return x * x;
}
常量表达式函数必须返回一个常量表达式,函数体中只能有一条返回语句,且不能包含任何形式的循环、分支(除了条件运算符)等。
声明类构造函数:允许类类型在编译时被初始化。构造函数体必须为空,所有成员初始化都必须使用常量表达式。
class Point {
public:
constexpr Point(double xVal = 0, double yVal = 0) : x(xVal), y(yVal) {}
constexpr double getX() const { return x; }
constexpr double getY() const { return y; }
private:
double x, y;
};
void main(){
constexpr Point p(9.0, 27.0);
constexpr double x = p.getX(); // 在编译时计算
}
使用 constexpr 的优点包括:
-
性能提升:通过在编译时而不是运行时计算值,可以提高程序的运行效率。
-
类型安全:与宏相比,
constexpr
提供了更强的类型安全。 -
更广泛的用途:
constexpr
变量、函数和对象可以用在需要编译时常量的上下文中,如数组大小、模板参数等。
12. 默认和删除函数
C++11引入了两个重要的特性:允许显式地声明默认构造函数和析构函数,以及允许删除函数。这些特性提供了对类行为更细致的控制,特别是在管理资源、实现单例模式或防止对象拷贝时非常有用。
默认函数(= default)
在C++11之前,如果你希望类有一个默认的构造函数、拷贝构造函数、拷贝赋值运算符或析构函数,你通常不需要做任何事情;编译器会为你自动生成这些。然而,一旦你定义了任何构造函数,编译器就不会自动生成默认构造函数了。C++11通过= default
关键字允许你显式地要求编译器为你生成这些函数,即使你已经定义了其他构造函数。
语法:
使用关键字 = default
来声明
class ClassName {
public:
ClassName() = default; // 默认构造函数
ClassName(const ClassName&) = default; // 默认拷贝构造函数
ClassName& operator=(const ClassName&) = default; // 默认拷贝赋值运算符
~ClassName() = default; // 默认析构函数
};
代码示例:
class MyClass {
public:
MyClass() = default; // 显式声明使用编译器生成的默认构造函数
MyClass(int value) : data(value) {} // 自定义构造函数
// ...
private:
int data;
};
删除函数(= delete)
C++11允许你显式地禁用类的某些函数(比如拷贝构造函数或拷贝赋值运算符),只需将它们声明为= delete
。这对于防止对象被无意拷贝或赋值非常有用,尤其在设计只能移动不能拷贝的资源管理类时。
语法:
class ClassName {
public:
ClassName(const ClassName&) = delete; // 禁用拷贝构造函数
ClassName& operator=(const ClassName&) = delete; // 禁用拷贝赋值运算符
};
代码示例:
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete; // 禁止拷贝
NonCopyable& operator=(const NonCopyable&) = delete; // 禁止赋值
// ...
};
总结:
= default
和= delete
是C++11中引入的两个关键特性,它们提供了对类默认行为的显式控制。通过= default
,你可以明确地告诉编译器为类生成默认的构造函数、析构函数或拷贝/赋值运算符,即使定义了其他构造函数。通过= delete
,你可以防止类的拷贝或赋值,这在设计不可拷贝的资源管理类或单例类时特别有用。这两个特性让类的设计意图更加清晰,同时也有助于避免潜在的错误。
13. 委托构造函数
在 C++11 中,委托构造函数(Delegating Constructors)是一种允许一个构造函数在同一个类中调用另一个构造函数的功能,目的是为了减少代码重复,提高代码复用性。这允许构造函数之间的代码共享,从而避免在每个构造函数中重复相同的初始化代码。
语法
委托构造函数的语法相当直接,就是在构造函数的初始化列表中调用同一个类的另一个构造函数。
class ClassName {
public:
ClassName(参数列表) : ClassName(其他参数列表) {
// 构造函数体
}
};
这里,构造函数通过在其初始化列表中调用另一个构造函数(即委托给另一个构造函数),实现对对象的初始化。
代码示例
考虑一个简单的 Rectangle 类,它有两个成员变量:长度和宽度。我们可以使用委托构造函数来确保所有的构造逻辑都通过一个主要的构造函数来执行,避免代码的重复。
class Rectangle {
public:
// 主构造函数
Rectangle(double width, double height) : width(width), height(height) {
// 这里可以包含一些特定的初始化逻辑
std::cout << "Rectangle(double, double)" << std::endl;
}
// 委托构造函数,委托给主构造函数
Rectangle(double side) : Rectangle(side, side) {
// 注意:这里的初始化逻辑会在主构造函数之后执行
std::cout << "Rectangle(double)" << std::endl;
}
void area() const {
std::cout << "Area: " << width * height << std::endl;
}
private:
double width, height;
};
int main() {
Rectangle square(5); // 使用委托构造函数
square.area(); // 输出: Area: 25
Rectangle rectangle(4, 5); // 使用主构造函数
rectangle.area(); // 输出: Area: 20
return 0;
}
在这个例子中,Rectangle类有三个构造函数:
-
一个是接受两个参数(宽度和高度)的主构造函数。
-
另外两个是委托构造函数,一个不带参数,默认构造一个宽度和高度都为0的矩形;另一个只带一个参数,构造一个正方形。
这样的设计让构造函数的初始化逻辑更加集中,如果需要修改初始化逻辑,只需要修改主构造函数即可,提高了代码的可维护性。
14. 继承构造函数
在C++11中,引入了继承构造函数的概念,这允许派生类继承并直接使用基类的构造函数,而不需要在派生类中重新定义相同的构造函数。这个特性通过简化代码,避免不必要的重复,提高了代码的可维护性。
语法
要在派生类中继承基类的构造函数,你可以使用using
声明。基本语法如下:
class Derived : public Base {
public:
using Base::Base;
};
这里,Derived 类通过 using Base::Base;
声明,继承了 Base 类所有的构造函数。
代码示例
考虑以下基类Person,它有一个构造函数,接受一个表示人名的字符串参数:
#include <iostream>
#include <string>
class Person {
public:
std::string name;
Person(std::string n) : name(n) {
std::cout << "Person(" << name << ")" << std::endl;
}
};
现在,我们定义一个 Employee 类,它是 Person 的派生类,并且我们想让 Employee 类能够直接使用 Person 类的构造函数:
class Employee : public Person {
public:
using Person::Person; // 继承构造函数
void printName() {
std::cout << "Employee Name: " << name << std::endl;
}
};
接着,我们可以这样使用 Employee 类:
int main() {
Employee emp("John Doe");
emp.printName(); // 输出:Employee Name: John Doe
return 0;
}
在这个例子中,Employee 类继承了 Person 类的构造函数,所以我们可以直接使用一个字符串参数来构造 Employee 对象。这就避免了在 Employee 类中重新定义一个接受相同参数的构造函数。