文章目录
-
- C++11新特性详解(一):基础特性与类的增强
- 一、C++11的发展历程
-
- [1.1 历史背景](#1.1 历史背景)
- [1.2 C++11的重要意义](#1.2 C++11的重要意义)
- 二、列表初始化:统一的初始化语法
-
- [2.1 C++98时代的初始化](#2.1 C++98时代的初始化)
- [2.2 C++11的统一初始化](#2.2 C++11的统一初始化)
- [2.3 列表初始化的优势](#2.3 列表初始化的优势)
- [2.4 std::initializer_list](#2.4 std::initializer_list)
- 三、新的类功能增强
-
- [3.1 默认成员函数的回顾](#3.1 默认成员函数的回顾)
- [3.2 编译器生成移动构造和移动赋值的条件](#3.2 编译器生成移动构造和移动赋值的条件)
- [3.3 成员变量的就地初始化](#3.3 成员变量的就地初始化)
- [3.4 default和delete关键字](#3.4 default和delete关键字)
- 四、总结与展望
-
- [4.1 本文要点回顾](#4.1 本文要点回顾)
- [4.2 实践建议](#4.2 实践建议)
- [4.3 下一篇预告](#4.3 下一篇预告)
- 五、练习与思考
-
- [5.1 练习题](#5.1 练习题)
- [5.2 思考题](#5.2 思考题)
C++11新特性详解(一):基础特性与类的增强
💬 欢迎讨论:C++11是C++发展史上的重要里程碑,带来了大量激动人心的新特性。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!
👍 点赞、收藏与分享:如果这篇文章对你有帮助,记得点赞、收藏并分享给更多的朋友!
🚀 系列导航:本文是C++11新特性系列的第一篇,将带你了解C++11的基础特性和类功能的增强,为后续深入学习更复杂的特性打下坚实基础。
一、C++11的发展历程
1.1 历史背景
C++11是C++的第二个主要版本,也是自C++98以来最重要的更新。这个版本的诞生经历了漫长的等待------从C++03到C++11,中间间隔了整整8年时间,这也是C++历史上最长的版本间隔。
在C++11正式发布之前,人们曾使用"C++0x"这个名字来称呼它,因为当时预期它会在2010年之前发布(0x代表2000年到2009年之间的某一年)。然而,由于特性的复杂性和标准化工作的严谨性,最终这个版本直到2011年8月12日才被ISO正式采纳。
C++11的发布标志着C++进入了现代化时代。它引入了大量的新特性,不仅标准化了许多既有的实践,还显著改进了C++程序员可用的抽象能力。从C++11开始,C++标准委员会建立了一个新的节奏:每3年更新一次标准,于是我们陆续看到了C++14、C++17、C++20等版本的发布。
1.2 C++11的重要意义
C++11的重要性体现在以下几个方面:
语言表达能力的提升
C++11引入了许多新的语言特性,使得代码更加简洁、易读和易维护。例如auto关键字、范围for循环、lambda表达式等,这些特性大大减少了样板代码的编写。
性能的优化
通过引入右值引用和移动语义,C++11在不牺牲性能的前提下,提供了更优雅的资源管理方式。这对于需要高性能的应用来说是一个重大突破。
泛型编程的增强
可变参数模板的引入使得泛型编程变得更加灵活和强大,许多在C++98中难以实现或需要大量重复代码的功能,在C++11中可以用更简洁的方式实现。
标准库的扩充
C++11标准库增加了许多新的组件,如智能指针、线程库、正则表达式等,这些都是现代C++开发中不可或缺的工具。
二、列表初始化:统一的初始化语法
2.1 C++98时代的初始化
在C++98中,不同类型的对象有着不同的初始化方式,这种不统一性常常让初学者感到困惑。让我们先回顾一下传统的初始化方式:
cpp
struct Point
{
int _x;
int _y;
};
int main()
{
// 数组的初始化
int array1[] = {1, 2, 3, 4, 5};
int array2[5] = {0};
// 结构体的初始化
Point p = {1, 2};
// 普通变量的初始化
int x = 10;
int y(20);
return 0;
}
可以看到,数组和POD(Plain Old Data)类型的结构体可以使用大括号进行初始化,但普通的类类型对象却不支持这种语法。这种不一致性不仅增加了学习成本,也限制了语言的表达能力。
2.2 C++11的统一初始化
C++11试图实现"一切对象皆可用{}初始化"的目标,这种初始化方式也被称为列表初始化(List Initialization)。
内置类型的列表初始化
cpp
int x1 = {2}; // 使用等号的列表初始化
int x2{2}; // 不使用等号的列表初始化(C++11新增)
double d{3.14};
自定义类型的列表初始化
cpp
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
: _year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
// 列表初始化自定义类型
Date d1 = {2025, 1, 1}; // 调用构造函数
Date d2{2025, 1, 1}; // 省略等号
// 引用绑定临时对象
const Date& d3 = {2024, 7, 25};
// C++98就支持的单参数隐式转换
Date d4 = {2025};
Date d5 = 2025; // 不使用花括号也可以(单参数)
return 0;
}
列表初始化的原理
这里的Date d1 = {2025, 1, 1}本质上经历了以下过程:
- 使用
{2025, 1, 1}构造一个Date临时对象 - 临时对象拷贝构造d1
- 编译器优化:将两步合并为一步,直接用
{2025, 1, 1}构造d1
我们可以通过运行代码来验证:由于编译器优化,实际只会调用一次构造函数,不会调用拷贝构造。
2.3 列表初始化的优势
容器插入的便利性
列表初始化在使用容器时特别方便,尤其是在插入复杂对象时:
cpp
#include <vector>
using namespace std;
int main()
{
vector<Date> v;
Date d1(2025, 1, 1);
// 方式一:传递已有对象(拷贝构造)
v.push_back(d1);
// 方式二:传递匿名对象(移动构造,后续会详细讲)
v.push_back(Date(2025, 1, 1));
// 方式三:使用列表初始化(最简洁!)
v.push_back({2025, 1, 1});
return 0;
}
相比前两种方式,列表初始化的语法更加简洁,不需要显式写出类型名。
防止窄化转换
列表初始化还有一个重要的优势:它会阻止可能导致数据丢失的隐式类型转换(窄化转换):
cpp
int x = 3.14; // 允许,但会截断小数部分
int y = {3.14}; // 编译错误!列表初始化不允许窄化转换
这个特性可以帮助我们在编译时发现潜在的bug。
2.4 std::initializer_list
列表初始化虽然已经很方便了,但对于容器的初始化,如果我们想要用任意多个值来初始化一个容器,该怎么办呢?
问题场景
cpp
vector<int> v1 = {1, 2, 3};
vector<int> v2 = {1, 2, 3, 4, 5};
vector<int> v3 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
如果没有特殊的机制,我们需要为不同数量的参数实现不同的构造函数,这显然是不现实的。
std::initializer_list的引入
C++11标准库引入了std::initializer_list类模板来解决这个问题。它的本质是:
- 在底层开辟一个数组
- 将初始化列表中的数据拷贝到这个数组中
initializer_list对象内部有两个指针,分别指向数组的开始和结束位置
内部结构示意
cpp
template<class T>
class initializer_list
{
private:
const T* _begin;
const T* _end;
public:
size_t size() const { return _end - _begin; }
const T* begin() const { return _begin; }
const T* end() const { return _end; }
// ...
};
在容器中的应用
STL容器都增加了接受initializer_list参数的构造函数:
cpp
// vector的initializer_list构造函数
template<class T>
class vector
{
public:
typedef T* iterator;
// 接受initializer_list的构造函数
vector(initializer_list<T> l)
{
// 预留空间
reserve(l.size());
// 遍历initializer_list中的元素并插入
for (auto e : l)
push_back(e);
}
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};
使用示例
cpp
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;
int main()
{
// 查看initializer_list的大小
std::initializer_list<int> mylist;
mylist = {10, 20, 30};
cout << sizeof(mylist) << endl; // 通常是两个指针的大小
int i = 0;
cout << mylist.begin() << endl;
cout << mylist.end() << endl;
cout << &i << endl;
// 容器的列表初始化
vector<int> v1({1, 2, 3, 4, 5});
vector<int> v2 = {1, 2, 3, 4, 5}; // 语法糖,更简洁
const vector<int>& v3 = {1, 2, 3, 4, 5};
// map的列表初始化(结合pair的列表初始化)
map<string, string> dict = {
{"sort", "排序"},
{"string", "字符串"},
{"insert", "插入"}
};
// 赋值运算符也支持initializer_list
v1 = {10, 20, 30, 40, 50};
return 0;
}
两种初始化方式的语义差异
cpp
vector<int> v1({1, 2, 3, 4, 5}); // 直接构造
vector<int> v2 = {1, 2, 3, 4, 5}; // 构造临时对象 + 拷贝构造(编译器可能优化为直接构造)
const vector<int>& v3 = {1, 2, 3, 4, 5}; // 构造临时对象,v3引用该临时对象
虽然语义上有差异,但现代编译器通常会将第二种情况优化为直接构造。
initializer_list 会由编译器生成一个临时数组,initializer_list 内部仅保存 begin/end指针。该数组的存储位置由编译器决定,但其生命周期至少覆盖整个 full-expression(完整表达式)。
注:如果绑定到 const&,临时对象的生命周期会延长。
三、新的类功能增强
3.1 默认成员函数的回顾
在讲解C++11的新功能之前,让我们先回顾一下C++98中类的默认成员函数:
C++98的6个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符重载
- 取地址运算符重载
- const取地址运算符重载
其中,前4个最为重要,后2个在实际开发中很少需要自己实现。所谓"默认成员函数",是指如果我们不显式实现,编译器会自动生成一个默认版本。
C++11的新增默认成员函数
C++11新增了两个默认成员函数:
- 移动构造函数
- 移动赋值运算符重载
这两个函数与右值引用和移动语义密切相关,我们将在本系列的第二篇文章中详细讲解。
3.2 编译器生成移动构造和移动赋值的条件
虽然移动语义的详细内容会在下一篇讲解,但这里我们需要了解编译器何时会为我们自动生成移动构造和移动赋值函数。
移动构造的生成条件
编译器会自动生成默认移动构造函数,当且仅当:
- 你没有自己实现移动构造函数
- 你没有实现析构函数
- 你没有实现拷贝构造函数
- 你没有实现拷贝赋值运算符重载
移动赋值的生成条件
编译器会自动生成默认移动赋值运算符,条件与移动构造相同:
- 你没有自己实现移动赋值运算符
- 你没有实现析构函数
- 你没有实现拷贝构造函数
- 你没有实现拷贝赋值运算符重载
以上为常见规则,具体以标准为准;当成员/基类不可移动时,隐式生成的移动可能会被定义为 deleted。
默认移动函数的行为
默认生成的移动构造和移动赋值对不同类型的成员有不同的处理方式:
-
内置类型成员:移动等价于拷贝(逐成员复制值,浅拷贝)
-
自定义类型成员:
- 如果成员类型实现了移动构造/移动赋值,则调用移动操作
- 如果没有实现移动操作,则调用拷贝操作
示例代码
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
: _name(name)
, _age(age)
{}
// 如果不实现下面三个函数中的任何一个,
// 编译器会自动生成默认的移动构造和移动赋值
/*
Person(const Person& p)
: _name(p._name)
, _age(p._age)
{}
Person& operator=(const Person& p)
{
if (this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}
~Person()
{}
*/
private:
std::string _name; // 有移动构造
int _age; // 内置类型,按字节拷贝
};
int main()
{
Person s1;
Person s2 = s1; // 拷贝构造
Person s3 = std::move(s1); // 移动构造(如果有的话)
Person s4;
s4 = std::move(s2); // 移动赋值(如果有的话)
return 0;
}
重要规则
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。这是为了防止意外的拷贝操作影响性能。
3.3 成员变量的就地初始化
C++11允许在类定义时直接给非静态成员变量提供默认值,这个特性被称为就地初始化 或成员初始化。
C++98的做法
cpp
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
在C++98中,我们只能通过构造函数的初始化列表来初始化成员变量。
C++11的改进
cpp
class Date
{
public:
// 如果构造函数没有在初始化列表中显式初始化某个成员,
// 则使用声明时的默认值
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
// 这个构造函数没有初始化列表,
// 所有成员都会使用声明时的默认值
Date()
{}
private:
int _year = 1900; // C++11:就地初始化
int _month = 1;
int _day = 1;
};
就地初始化的优势
- 代码更简洁:不需要在每个构造函数中都写初始化列表
- 减少遗漏:确保成员变量始终有一个合理的初始值
- 提高可维护性:修改默认值时只需要改一个地方
初始化的优先级
如果同时存在就地初始化和初始化列表中的初始化,初始化列表的优先级更高:
cpp
class Example
{
public:
Example()
: _x(100) // 这个会覆盖就地初始化的值
{}
private:
int _x = 10; // 就地初始化
};
int main()
{
Example e;
// e._x 的值是 100,不是 10
return 0;
}
3.4 default和delete关键字
C++11引入了两个新关键字,让我们能够更精确地控制默认函数的生成。
=default:显式要求编译器生成默认版本
使用场景
当我们实现了某些特殊成员函数后,编译器可能不再自动生成其他默认函数。使用=default可以显式要求编译器生成默认版本。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
: _name(name)
, _age(age)
{}
// 提供了拷贝构造,编译器就不会生成移动构造了
Person(const Person& p)
: _name(p._name)
, _age(p._age)
{}
// 使用default显式要求编译器生成默认的移动构造
Person(Person&& p) = default;
private:
bit::string _name;
int _age;
};
int main()
{
Person s1("张三", 20);
Person s2 = s1; // 调用拷贝构造
Person s3 = std::move(s1); // 调用移动构造(default生成的)
return 0;
}
=delete:禁止编译器生成默认版本
使用场景
有时我们希望禁止某些操作,比如禁止拷贝。在C++98中,我们通常将函数声明为private并且不实现:
cpp
// C++98的做法
class NoCopy
{
private:
NoCopy(const NoCopy&); // 只声明不实现
NoCopy& operator=(const NoCopy&);
public:
NoCopy() {}
};
这种方法的缺点是:如果类的成员函数或友元函数试图调用拷贝,会产生链接错误而不是编译错误。
C++11提供了更好的解决方案:
cpp
// C++11的做法
class NoCopy
{
public:
NoCopy() {}
// 显式删除拷贝构造和拷贝赋值
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
};
int main()
{
NoCopy nc1;
// NoCopy nc2 = nc1; // 编译错误!拷贝构造被删除
NoCopy nc3;
// nc3 = nc1; // 编译错误!拷贝赋值被删除
return 0;
}
=delete的其他用途
=delete不仅可以用于特殊成员函数,还可以用于普通函数来禁止特定的重载或类型转换:
cpp
class SmartInt
{
public:
SmartInt(int val) : _val(val) {}
// 禁止从double隐式转换为SmartInt
SmartInt(double) = delete;
private:
int _val;
};
int main()
{
SmartInt si1(10); // OK
// SmartInt si2(3.14); // 编译错误!double版本被删除
return 0;
}
两个关键字的对比
| 特性 | =default | =delete |
|---|---|---|
| 作用 | 要求生成默认实现 | 禁止调用该函数 |
| 使用位置 | 只能用于特殊成员函数 | 可用于任何函数 |
| 错误时机 | - | 编译时 |
| C++98替代方案 | 无 | 声明为private |
四、总结与展望
4.1 本文要点回顾
在本文中,我们学习了C++11的基础特性:
列表初始化
- 统一了各种类型的初始化语法
- 使用
{}可以初始化任何类型的对象 std::initializer_list支持任意数量参数的容器初始化- 可以防止窄化转换,提高代码安全性
类功能的增强
- 新增了移动构造和移动赋值两个默认成员函数
- 支持成员变量的就地初始化,代码更简洁
=default可以显式要求生成默认函数=delete可以禁止函数的调用,提供更精确的控制
4.2 实践建议
优先使用列表初始化
在C++11及以后的代码中,建议优先使用列表初始化:
cpp
// 推荐
vector<int> v = {1, 2, 3, 4, 5};
Date d{2025, 1, 1};
// 而不是
vector<int> v;
v.push_back(1);
v.push_back(2);
// ...
合理使用就地初始化
对于有明确默认值的成员变量,使用就地初始化可以让代码更清晰:
cpp
class Config
{
private:
int _timeout = 30; // 默认超时时间
bool _autoRetry = true; // 默认自动重试
string _logPath = "/var/log/app.log"; // 默认日志路径
};
明确你的意图
使用=default和=delete明确表达你的设计意图:
cpp
class Singleton
{
public:
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton() = default;
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
};
4.3 下一篇预告
在下一篇文章中,我们将深入讲解C++11最重要的特性之一:右值引用与移动语义。这是C++11中最具革命性的特性,它从根本上改变了C++处理对象复制和资源管理的方式。
我们将学习:
- 左值和右值的本质区别
- 什么是右值引用,为什么需要它
- 移动构造和移动赋值的实现原理
- 如何在实际项目中应用移动语义提升性能
- 完美转发和引用折叠的高级话题
五、练习与思考
5.1 练习题
练习1:列表初始化
实现一个Matrix类,支持使用二维初始化列表进行初始化:
cpp
Matrix m = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
练习2:就地初始化
设计一个配置类,使用就地初始化提供默认值,并实现一个Builder模式来修改这些值。
练习3:禁用特定操作
实现一个FileHandle类,该类只能通过工厂函数创建,不允许拷贝,但允许移动。
5.2 思考题
- 列表初始化和传统初始化在性能上有区别吗?
- 为什么提供了移动构造后,编译器就不再自动生成拷贝构造了?
=delete和将函数声明为private有什么本质区别?
通过本文的学习,我们掌握了C++11的基础特性。这些特性虽然看起来简单,但它们是现代C++代码的基础。在下一篇文章中,我们将学习更加深入和强大的特性------右值引用与移动语义,这将带给你对C++全新的理解!
以上就是C++11基础特性的全部内容,期待在下一篇文章中与你继续探讨右值引用与移动语义!如有疑问,欢迎在评论区交流讨论!❤️