一、C++语言发展介绍
设计C++是为了解决一个问题:如何直接操作硬件,同时又支持高效、高级的抽象。、
组成部分:语言、标准库、许多其它的库、庞大的旧代码、工具
C++开始于1979年4月,一个带类的C。
1979年:new和delete函数
1981年:const-支持接口和符号常量的不变性
1982年:虚函数-提供运行期多态
1984年:引用-支持运算符重载和简化的参数传递
运算符和函数重载-除了算法和逻辑运算符外,还包括:赋值=、调用()、下表[]和智能指针->。
1987年:类型安全连接-消除许多来自不同翻译单元中不一致的声明错误
抽象类-提供纯接口。
1980:
模板-经过多年使用宏进行泛型编程之后,模板更好的支持泛型编程
异常-试图给混乱的错误带来某种秩序
二、C++语言总结
更好的C
支持数据抽象
支持面向对象编程
支持泛型编程
三、C++98
模板:无约束、图灵完备、对泛型编程的编译器支持
异常:一套在单独路径上返回错误值的机制,由调用方栈顶上的“在别处”的代码处理
namespace:命名空间。允许程序员在编写独立部分组成的较大程序时,避免名称的冲突
条件语句的声明:写法紧凑、限制变量作用域
具名 类型转换:static_cast、
bool:布尔类型,曾经使用int作为布尔变量和常量
一个文件打开操作
{
FILE* P = fopen(name,"r");
fclose(p);
}
文件句柄的泄露会比内存泄漏更快的耗尽 操作系统的资源
C++98标准库提供了:
STL:创造性的、通用的、优雅的、高效的容器、迭代器 和算法框架
特征 (trait):对使用模板编程有用的编译器属性集
string:保存和操作字符序列的类型。字符类型是一个模板参数,默认值为char
iostream:数据流。处理字符类型、区域设置、缓冲区策略
bitset:保存和操作比特位集合的类型
locale:处理不同文化传统的精致框架,主要与输入输出有关
valarray:一个数值数组。很少被使用
auto_ptr:早期代表独占所有权的指针;在C++11中被shared_ptr和unique_ptr替代
四、C++11
C++11引入了大量令人眼花缭乱的东西
内存模型:一个高效的现代硬件设计的底层抽象,作为描述并非的基础
auto和decltype:避免类型名称和不必要的冲突
范围for:对范围的简单顺序遍历
移动语义和右值引用:目的是减少数据拷贝
统一初始化:初始化语法
nullptr:给空指针一个名字
constexpr:在编译器进行求值的函数
用户定义字面常量:为用户自定义的类型,提供字面常量
原始字符串字面量:不需要转义字符的字面量,主要用在正则表达式中
属性:将任意信息同一个名字关联。
lambda表达式:匿名函数
变参模板:可以处理任意个任意类型的参数的模板
模板别名:能够重命名模板并为新名称绑定一些模板参数
noexcept:确保函数不会抛出异常,提高效率
override和final:管理大型类层次结构的语法
static_assert:编译器断言
long long:更长的整数类型
默认成员初始化器:给数据成员一个默认值,这个默认值可以被构造函数中的初始化覆盖
enum class:枚举类型定义带有作用域的强制类型枚举
标准库组件
unique_ptr和shared_ptr依赖RAII的资源管理指针
内存模型和atomic变量
thread、mutex、condition_variable等为基本的系统层级的并发提供了类型安全、可移植的支持。
future、promise和packaged_task稍稍高级的并发
tuple匿名的简单复合类型
类型特征-类型的可测试属性,用于元编程
正则表达式匹配
随机数-带有许多生成器和多种分布
时间-time_point和duration
unordered_map哈希表
forward_list单向链表
array具有固定常量大小的数组,并且会记住自己的大小
emplace运算-在容器内直接构造对象,避免拷贝
exception_ptr允许在线程之间传递异常
一些看似不连贯的语法规则,互不相关的扩展怎么能组成一个连贯的整体呢。当你使用的多了,融入到你的日常编码中,会发现他们节省了开发时间。
总结
支持并发
简化使用
改进对泛型编程的支持
增加静态类型安全
支持对库的开发
标准库组件
五、C++11:并发支持
内存模型
atomic类型
atomic<int> x;
int n;
void main()
{
x++;
n++;
}
mutex
mutex mt;
atomic<bool> b;
atomic<int> x;
if(b)
{
lock_guard<mutex> lock(mt);
if(b)
{
x++;
}
} // 自动释放mt锁
lock_guard 是一种RAII类型,确保会解锁它所控制的mutex。
锁的开销比atomic大的多的多。
无锁编程才是专家级别的工作
六、线程和锁
thread:系统的执行线程,支持join()和detach()
mutex:系统的互斥锁,支持lock()和unlock(),要保证unlock()
condition_variable:系统中线程间进行事件通信的条件变量
thread_local:线程本地存储
#include <iostream>
#include <thread>
void printMessage(const std::string& message) {
std::cout << message << std::endl;
}
int main() {
std::thread t(printMessage, "Hello from thread with a message!");
t.join(); // 等待线程 t 完成
return 0;
}
匿名函数
#include <iostream>
#include <thread>
int main() {
std::thread t([]() {
std::cout << "Hello from a lambda thread!" << std::endl;
});
t.join(); // 等待线程 t 完成
return 0;
}
join()
:等待线程结束。在调用 join()
之前,主线程会阻塞,直到线程完成。
detach()
:允许线程独立运行。调用 detach()
后,线程会与主线程分离,继续在后台运行。
#include <iostream>
#include <thread>
void longRunningTask() {
// 执行一些长时间运行的任务
}
int main() {
std::thread t(longRunningTask);
t.detach(); // 线程 t 现在在后台运行
// 主线程会继续执行,而 t 线程在后台运行
return 0;
}
线程安全
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int sharedData = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++sharedData;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Shared data: " << sharedData << std::endl;
return 0;
}
注意事项:
- 使用
std::thread
时,确保在线程结束前调用join()
或detach()
,否则程序可能无法正确退出。 - 避免在线程销毁前没有调用
join()
或detach()
,这会导致未定义行为。 - 使用
std::lock_guard
或std::unique_lock
管理互斥锁,以确保线程安全。
七、最后C++11还提供多线程async()函数
async()函数,可以启动一个任务并在另一个thread上执行
future一个句柄,通过它可以从一个共享的单对象缓冲区中get()一个值,可能需要等待某个promise将该值放入缓冲区
promise一个句柄,通过它可以将一个值put()到一个共享的单对象缓冲区,可能会唤醒某个等待future的thread
packaged_task一个类,它使得设置一个函数在线程上异步执行变得容易,由future来接受promise返回结果
#include <future>
#include <iostream>
int asyncFunction() {
// 执行一些计算密集型任务
return 520; // 返回结果
}
int main() {
// 启动异步任务
std::future<int> result = std::async(std::launch::async, asyncFunction);
// 在此处可以执行其他任务...
// 获取异步任务的结果
int value = result.get(); // 阻塞直到异步任务完成
std::cout << "The answer is " << value << std::endl;
return 0;
}
处理异步任务的结果
std::future::get()
方法会阻塞调用线程,直到异步任务完成,并返回任务的结果。
错误处理
如果异步任务抛出异常,std::future::get()
会重新抛出该异常。
注意事项:
std::async
有两个参数:第一个是启动策略,第二个是要执行的任务。std::launch::async
确保任务在新线程中异步执行。- 如果不指定启动策略,
std::async
可能会立即同步执行任务,而不是异步。 std::future
对象在默认情况下是移动语义的,可以被移动,但不能被复制。std::future::get()
只能调用一次,因为调用后它会消费掉future
对象中的结果。
八、C++11举例
1、auto关键字
std::string str1 = "Hello";
std::string str2 = "World";
auto s = str1 +str2;
std::vecto<int> vec;
auto itor = vec.begin();
2、范围for
vector<int> vec;
for(auto itor : vec)
{
cout << itor <<endl;
}
int sum = 0;
for(auto i : {1,2,3,4,5,6,7,8,9})
{
sum++i;
}
3、移动语义
C++11 引入了移动语义,这是一种语言特性,它允许资源(如动态分配的内存、文件描述符、网络连接等)在不同对象之间高效地转移,而无需显式地复制。移动语义的主要目的是减少不必要的对象复制,从而提高程序的性能和效率。
-
移动构造函数
- 移动构造函数是一种特殊的构造函数,它接受一个将要被移动的对象的引用(通常是右值引用)作为参数,并将其资源“移动”到新对象中。
-
移动赋值运算符
- 移动赋值运算符允许将一个对象的资源移动到另一个对象中,而不是复制。
-
右值引用
- 右值引用使用
&&
符号声明,它允许程序员明确地表示一个对象是临时的,应该被移动而不是被复制。
- 右值引用使用
-
std::move
:std::move
是一个标准库函数,它将一个对象转换为右值引用,从而允许移动构造函数或移动赋值运算符被调用。
#include <iostream>
#include <vector>
class MyClass {
public:
std::vector<int> data;
// 移动构造函数
MyClass(MyClass&& other) : data(std::move(other.data)) {
std::cout << "Move constructor called" << std::endl;
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) {
if (this != &other) {
data = std::move(other.data);
std::cout << "Move assignment called" << std::endl;
}
return *this;
}
};
int main() {
MyClass a;
a.data.push_back(1);
MyClass b = std::move(a); // 调用移动构造函数
MyClass c;
c = std::move(b); // 调用移动赋值运算符
return 0;
}
MyClass
包含了一个 std::vector<int>
成员。移动构造函数和移动赋值运算符使用 std::move
来获取 data
成员的所有权,而不是复制它。这使得 b
和 c
能够高效地接收 a
的资源。
4、智能指针
shared_ptr:代表共享所有权
unique_ptr:代表独占所有权
5、统一初始化
统一初始化使用花括号 {}
包围初始化值,并且可以用于所有类型的初始化,包括基本数据类型、类对象、数组和聚合类型。
#include <iostream>
#include <vector>
class MyClass {
public:
int a;
double b;
std::string c;
MyClass(int a, double b, std::string c) : a(a), b(b), c(c) {}
};
int main() {
int n; // 默认初始化
int n1 = 520; // 值初始化
int x{10}; // 统一初始化
double y = {20.0}; // 统一初始化,也可以省略等号
std::vector<int> v{1, 2, 3, 4, 5}; // 统一初始化
MyClass obj{100, 200.5, "Hello"}; // 统一初始化,并且调用 MyClass 的构造函数
return 0;
}
6、nullptr 空指针
int *p = nullptr;
7、constexpr关键字
用于声明常量表达式。constexpr
的目的是在编译时计算表达式的值,而不是在运行时,这样可以提高程序的效率,并且使得一些在编译时才能确定的值能够用于定义常量。
constexpr
的特点:
-
编译时计算:
constexpr
修饰的变量或函数必须在编译时就能确定其值,这样才能保证在编译时进行计算。 -
常量表达式:
constexpr
修饰的变量必须是一个常量表达式,即它们的值在编译时就能确定,且不会改变。 -
模板参数:
constexpr
可以用于模板参数,使得模板能够根据编译时已知的值进行实例化。 -
内联函数:
constexpr
函数在调用时会像内联函数一样,将其代码直接替换到调用点。
constexpr
的应用:
-
定义常量:
constexpr int maxConnections = 10;
-
定义复杂常量表达式:
constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }
-
用于模板参数:
template <constexpr int N> void printFactorial() { std::cout << "Factorial of " << N << " is " << factorial(N) << std::endl; }
-
定义
constexpr
函数:constexpr double square(double x) { return x * x; }
constexpr
的限制:
-
运行时不可用:
constexpr
函数不能包含运行时才能确定的表达式,如调用非constexpr
函数。 -
不能抛出异常:
constexpr
函数不能包含可能抛出异常的代码。 -
不能有多个定义:
constexpr
函数只能有一个定义,通常在头文件中定义。
示例:
#include <iostream>
constexpr int add(int a, int b) {
return a + b;
}
constexpr int multiply(int a, int b) {
return a * b;
}
int main() {
constexpr int sum = add(1, 2);
constexpr int product = multiply(3, 4);
std::cout << "Sum: " << sum << std::endl;
std::cout << "Product: " << product << std::endl;
return 0;
}
add
和 multiply
函数都被声明为 constexpr
,这意味着它们必须在编译时就能计算出结果。在 main
函数中,sum
和 product
变量也是在编译时就确定了值。
8、别名
typedef int (*pf)(int); // pf是一个函数指针
9、tuple元组
存储不同类型的数据项
#include <iostream>
#include <tuple>
int main() {
// 创建一个包含 int, double, std::string 的 tuple
std::tuple<int, double, std::string> myTuple = {10, 3.14, "Hello"};
// 访问 tuple 中的元素
std::cout << "Int: " << std::get<0>(myTuple) << std::endl;
std::cout << "Double: " << std::get<1>(myTuple) << std::endl;
std::cout << "String: " << std::get<2>(myTuple) << std::endl;
// 使用 std::tie 来解包 tuple
int i;
double d;
std::string s;
std::tie(i, d, s) = myTuple;
std::cout << "Using std::tie: " << i << ", " << d << ", " << s << std::endl;
// 使用 std::apply 来调用函数
std::cout << "Sum: " << std::apply([](int x, double y, const std::string& z) {
return x + y + z.length();
}, myTuple) << std::endl;
return 0;
}
10、enum class
enum class
或enum struct
语法,这被称为强类型枚举或枚举类。这种新的枚举定义方式提供了更好的类型安全,并且可以避免枚举值在不同枚举类型之间的隐式转换。
在 C++11 之前,枚举是这样定义的:
enum Color { RED, GREEN, BLUE };
这种定义方式存在一些问题,例如枚举值的类型不明确,它们默认会被提升为整型,并且枚举值可以隐式转换为整数,这可能导致类型安全问题。
C++11 枚举类定义:
C++11 引入了枚举类,使用 enum class
或 enum struct
关键字:
enum class Color { RED, GREEN, BLUE };
或者:
enum struct Color { RED, GREEN, BLUE };
这两种方式是等价的,枚举类提供了以下优势:
-
类型安全:枚举类的成员不能隐式转换为整数类型,这避免了与整数的意外混用。
-
命名空间隔离:枚举类的成员不会污染全局命名空间,它们的访问需要通过枚举类型名作为前缀。
-
显式转换:如果需要将枚举值转换为整数,可以显式地进行转换。
#include <iostream>
enum class Color { RED, GREEN, BLUE };
int main() {
Color myColor = Color::RED;
// 需要显式转换为整数
int redAsInt = static_cast<int>(myColor);
std::cout << "My color is " << redAsInt << std::endl;
// 比较枚举值
if (myColor == Color::RED) {
std::cout << "The color is red." << std::endl;
}
return 0;
}