以点带面学习C++ 第二章
文章目录
四种类型转换
static_cast
static_cast 是 C++ 中最常用的类型转换操作符,它用于静态转换,适用于编译时已知类型的转换。static_cast 主要用于:
①基本数据类型之间的转换,如 int 到 float 或 char 到 int 等。
②类层次结构中的指针或引用的转换(例如从基类指针到派生类指针),只要转换是合法的。
③进行 void* 和其他指针类型之间的转换。
static_cast 的转换是在编译时完成的,因此它的类型检查是静态的,可以避免某些不合适的转换,但不会做运行时检查。对于不安全的转换,static_cast 会引发编译错误。
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class" << endl;
}
};
int main() {
Base *basePtr = new Derived();
// 使用 static_cast 转换类型
Derived *derivedPtr = static_cast<Derived*>(basePtr);
derivedPtr->show(); // 输出: Derived class
return 0;
}
在上述代码中,static_cast 用于将基类指针 basePtr 转换为派生类指针 derivedPtr。因为 basePtr 实际上指向一个 Derived 类对象,所以转换是合法的。
注意事项:
static_cast 不检查转换是否合法,如果你试图将一个类型转换成不兼容的类型,编译器会报错。此外,static_cast 适用于静态类型信息已知的转换,不能用于动态类型检查。
dynamic_cast
dynamic_cast 是 C++ 中用于动态类型转换的操作符,主要用于 类层次结构 中的指针或引用的转换。它在运行时进行类型检查,特别适用于带有虚函数的类,通常用于 向下转换(从基类到派生类)或 向上转换(从派生类到基类)。
dynamic_cast 主要的特点是:
①运行时类型检查:dynamic_cast 会检查转换是否安全,如果不安全,它会返回 nullptr(对于指针类型)或抛出 std::bad_cast 异常(对于引用类型)。
②适用于带有虚函数的类层次结构,利用 RTTI(运行时类型识别)来进行类型检查。
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class" << endl;
}
};
int main() {
Base *basePtr = new Base();
Derived *derivedPtr = dynamic_cast<Derived*>(basePtr); // 使用 dynamic_cast
if (derivedPtr) {
derivedPtr->show();
} else {
cout << "Conversion failed!" << endl;
}
return 0;
}
在上述代码中,basePtr 指向一个 Base 类型的对象,而我们尝试将它转换为 Derived 类型。由于 basePtr 实际上并不指向 Derived 类型的对象,dynamic_cast 返回了 nullptr,并输出 “Conversion failed!”。
注意事项:
①必须有虚函数:dynamic_cast 仅在类层次结构中有虚函数时有效。
②返回 nullptr 或抛出异常:如果转换不合法,指针类型的 dynamic_cast 返回 nullptr,而引用类型的 dynamic_cast 会抛出 std::bad_cast 异常。
③适用于多态类:dynamic_cast 用于实现多态类型之间的安全转换,常用于动态类型检查。
const_cast
const_cast 用于移除或添加常量性。它是 C++ 中唯一能够修改常量对象或常量指针的转换操作符。通过 const_cast 可以去掉常量属性,使得原本为 const 的指针或引用变得可修改,const_cast 也可以将非 const 类型转换为 const 类型。
#include <iostream>
using namespace std;
void modify(const int &x) {
// 通过 const_cast 移除常量性
int &y = const_cast<int&>(x);
y = 20;
cout << "Modified value: " << y << endl;
}
int main() {
int num = 10;
modify(num); // 可以修改 const 引用
cout << "Original value: " << num << endl; // 输出修改后的值 20
return 0;
}
在这个例子中,modify 函数接收一个 const int& 类型的引用,使用 const_cast 去掉常量性,从而修改 x 的值。
注意事项:
①const_cast 仅改变常量属性,它不进行其他类型转换。
②不应修改原本是 const 的对象,否则可能导致未定义行为。你应该仅当对象本身并非真正的常量时才使用它。
reinterpret_cast
reinterpret_cast 是 C++ 中最危险的一种类型转换,它用于不同类型之间的位模式转换。reinterpret_cast 可以将一个指针转换为另一个类型的指针,或者将指针和整数之间进行转换。它提供了对位级别的操作,这意味着它能够将类型完全不相关的两者转换在一起,这种转换一般会导致不可预期的行为,因此要小心使用。
#include <iostream>
using namespace std;
int main() {
int num = 10;
// 将 int 指针转换为 char 指针
char* charPtr = reinterpret_cast<char*>(&num);
cout << "Byte representation of int: " << *charPtr << endl;
// 将指针转换为整数
uintptr_t ptrValue = reinterpret_cast<uintptr_t>(charPtr);
cout << "Pointer as integer: " << ptrValue << endl;
return 0;
}
在这个例子中,reinterpret_cast 将 int* 类型的指针转换为 char* 类型的指针,之后读取内存中的字节值。同时也将指针转换为整数表示。
注意事项:
①不可移植性:由于平台和硬件架构的差异,reinterpret_cast 的行为通常依赖于具体的硬件和操作系统。因此,它的使用可能导致移植性问题。
②危险性:由于它不执行任何检查,所以 reinterpret_cast 会非常容易出错,可能会导致内存错误、崩溃等未定义行为。
③只在必要时使用:这种转换通常用于底层编程或与硬件交互时,普通的应用程序中应尽量避免使用。
左值和右值
在 C++ 中,左值(Lvalues)和右值(Rvalues)是两个非常重要的概念,它们直接关系到内存管理、资源管理、以及 C++ 中的表达式求值和优化机制(如 移动语义 和 完美转发)。理解这两个概念是学习 C++ 高级特性(如 move 语义、std::move、std::forward 等)所必须的。
左值
左值(Lvalue)是一个可以出现在赋值语句左侧的值。它代表了内存中的某个位置,可以被修改或重新赋值。因此,左值有一个明确的内存地址,可以在程序的其他地方访问或修改。
定义:左值指的是那些有“持久”内存位置的对象(即它们在程序的生命周期中有一个明确的地址)。
①可以赋值:可以通过赋值操作修改它的值。
②可以取地址:可以用 & 操作符取到它的内存地址。
int x = 10; // x 是一个左值
x = 20; // 通过左值 x 修改它的值
int *p = &x; // x 的地址可以被取得
左值的特点:
①可取地址:任何左值都可以通过 & 操作符获取其内存地址。
②可以出现在赋值的左边:即使是常量变量,只有左值才能出现在赋值语句的左侧(右值不能)。
右值
右值(Rvalue)是没有持久内存位置的值,通常是临时的、不持久的,通常是表达式的结果,或者是一些即将被销毁的对象。
定义:右值表示一个临时对象或不再需要的对象,不能直接取地址,通常是一些计算后的临时结果。
①不可以赋值:右值不能出现在赋值语句的左侧,因为它没有持久的内存位置。
②不能取地址:大多数右值不能通过 & 操作符取得内存地址。
int x = 10;
int y = x + 5; // x + 5 是右值,表示表达式的结果
int *p = &(x + 5); // 错误:x + 5 是右值,不能取地址
右值的特点:
①不可取地址:你无法直接通过 & 操作符获取右值的内存地址。
②不可赋值:不能把右值赋给左值,只能把左值赋给左值。
③临时性:右值通常是临时存在的,可能在表达式计算完成后就会被销毁。
左值引用和右值引用
为了更好地处理左值和右值,C++ 引入了左值引用和右值引用这两个概念。
左值引用
左值引用是普通的引用类型,它允许你绑定到一个左值(即持久存在的对象)。
语法:T&,其中 T 是类型名。
用途:允许你修改左值,或者通过引用避免复制。
int a = 10;
int b = 20;
int& ref = a; // ref 是 a 的左值引用
ref = b; // 修改 a 的值为 b 的值
右值引用
右值引用是 C++11 引入的新特性,用于绑定到右值(临时对象)。右值引用提供了一种在传递临时对象时“窃取”其资源(例如内存)的方式,减少不必要的资源拷贝,从而提高程序的性能。
语法:T&&,其中 T 是类型名。
用途:实现移动语义(通过 std::move)以及完美转发(通过 std::forward)。
int&& rref = 10; // rref 是一个右值引用,绑定到右值 10
rref = 20; // 修改右值引用指向的对象的值(注意,这并不改变 10,而是改变 rref)
区分左值和右值的标准
左值:
具有持久的内存地址。
可以取地址。
可以出现在赋值语句的左侧。
右值:
没有持久的内存地址(或仅在短暂的时间内存在)。
不能取地址(除非是 std::move 或类似的操作)。
不能出现在赋值语句的左侧,只能在赋值语句的右侧出现。
右值与移动语义
C++11 引入的右值引用为“移动语义”的实现提供了支持。移动语义允许我们“移动”资源(如动态分配的内存、文件句柄等)而不是复制它们。这样,程序在处理大量数据时能够显著提高性能,避免不必要的复制操作。
std::move 是一个强制类型转换函数,它将一个左值转换为右值引用,从而启用移动语义。虽然它叫做“move”,但它只是把左值转换为右值引用,并不真的执行移动操作。
#include <iostream>
#include <vector>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "Constructor called!" << endl;
}
MyClass(const MyClass& other) {
cout << "Copy constructor called!" << endl;
}
MyClass(MyClass&& other) noexcept {
cout << "Move constructor called!" << endl;
}
};
int main() {
MyClass a;
MyClass b = std::move(a); // 调用移动构造函数,而不是拷贝构造函数
return 0;
}
在这个示例中,std::move(a) 将 a 转换成一个右值引用,并调用 MyClass 的移动构造函数。
移动构造函数
移动构造函数是为支持移动语义而编写的特殊构造函数,它接受一个右值引用作为参数,并把源对象的资源转移到新对象中,而不是复制。
MyClass(MyClass&& other) noexcept {
// 转移资源,而不是复制
}
右值引用与完美转发
右值引用和完美转发是 C++11 引入的重要概念,它们主要用于优化性能,尤其是在 C++ 中实现移动语义。理解这两个概念不仅能帮助你更高效地管理资源,还能避免不必要的对象复制操作,尤其是在处理大型数据结构或复杂对象时。std::forward 用于完美转发,std::forward 根据 T&& 是否是左值或右值引用来决定是传递左值引用还是右值引用。
template <typename T>
void wrapper(T&& arg) {
some_function(std::forward<T>(arg)); // 完美转发 arg
}
右值引用的用途
移动语义:通过右值引用可以实现将一个对象的资源从一个位置转移到另一个位置,而不是复制对象。对于复杂对象(例如动态分配内存的对象),移动语义能大幅提高性能。
完美转发:在模板函数中,右值引用允许我们完美地转发参数的值类别(左值或右值)。即保持原始对象的特性,避免不必要的复制。
#include <iostream>
#include <vector>
using namespace std;
class MyClass {
public:
MyClass() { cout << "Constructor called!" << endl; }
MyClass(const MyClass& other) { cout << "Copy Constructor called!" << endl; }
MyClass(MyClass&& other) noexcept { cout << "Move Constructor called!" << endl; }
};
int main() {
MyClass a; // 调用构造函数
MyClass b = std::move(a); // 调用移动构造函数,a 被“移动”
return 0;
}
//输出
//Constructor called!
//Move Constructor called!
在上面的例子中,std::move(a) 将 a 转换成右值引用,触发了 移动构造函数。
右值引用的语法
T&&:右值引用的声明语法。右值引用可以绑定到右值(临时对象)或左值(通过 std::move 转换成右值引用)。
为什么要使用右值引用?
①提高性能:右值引用使得能够从一个对象“窃取”其资源,而不是复制对象。当一个对象的资源非常重(例如大数据结构或动态分配的内存)时,使用右值引用可以减少昂贵的复制操作。
②资源管理:通过右值引用,我们可以显式地管理资源的转移。例如,在容器类中,std::vector 等会使用右值引用来实现移动操作,而不是深度复制对象。
完美转发
完美转发的概念
完美转发是指将函数参数的值类别(左值或右值)传递给另一个函数,保持参数的原始特性,而不进行不必要的复制或类型转换。在模板函数中,完美转发允许我们在将参数传递给其他函数时,能够保留传入参数的左值或右值特性。
完美转发的应用场景
假设你有一个包装函数,想将参数完美地传递给另一个函数。完美转发的关键在于:
①保留传递参数的值类别:即如果传递的是左值,就继续传递左值;如果传递的是右值,就继续传递右值。
②避免不必要的复制:完美转发能够避免多余的对象拷贝,提高性能。
std::forward 和完美转发
完美转发的实现通常依赖于 std::forward 函数,它在模板函数中使用右值引用时,可以完美地转发参数。
std::forward(arg) 会根据 T 的类型来决定是传递左值引用还是右值引用。如果 T 是左值引用类型,则转发时会继续传递左值引用;如果 T 不是左值引用类型,则转发时会传递右值引用。
#include <iostream>
#include <utility> // for std::forward
void printValue(int& x) {
std::cout << "Lvalue: " << x << std::endl;
}
void printValue(int&& x) {
std::cout << "Rvalue: " << x << std::endl;
}
template <typename T>
void forwardToPrint(T&& arg) {
printValue(std::forward<T>(arg)); // 完美转发
}
int main() {
int a = 10;
forwardToPrint(a); // 调用 printValue(int&)
forwardToPrint(20); // 调用 printValue(int&&)
return 0;
}
//输出
//Lvalue: 10
//Rvalue: 20
std::forward 的工作原理
std::forward 是通过条件判断来判断如何转发参数。它在模板函数中通过类型推导来保持传入参数的值类别。
左值引用:如果 T 是左值引用类型,则 std::forward 会返回一个左值引用。
非左值引用:如果 T 不是左值引用类型,则 std::forward 会返回一个右值引用
template<typename T>
T&& forward(T&& arg) {
return static_cast<T&&>(arg); // 通过类型转换来保持值类别
}
右值引用与完美转发的关系
右值引用主要用于绑定到右值,以实现移动语义,从而优化资源传递。完美转发使用右值引用和 std::forward 可以在模板函数中实现完美转发,保持传入参数的值类别,从而避免不必要的复制。
C++ 中的存储类
C++ 中的存储类(Storage Class)用于定义变量或函数的生命周期、作用域、存储位置等特性。它们决定了对象或函数的存储管理方式以及它们的可见性。存储类可以影响变量的作用域、链接、初始化方式、以及存储方式。C++ 提供了多种存储类,常见的存储类包括 auto、register、static、extern、mutable 和 thread_local。
auto 存储类
auto 关键字用于自动推导变量的类型,通常用于函数返回类型的推导或声明变量时让编译器推导类型。现代 C++ 中,auto 的主要用途是推导变量类型,尤其是复杂类型。
从 C++ 17 开始,auto 关键字不再是 C++ 存储类说明符,且 register 关键字被弃用。
auto x = 10; // x 会被推导为 int 类型
auto y = 3.14; // y 会被推导为 double 类型
在 C++11 之后,auto 也可以用于函数返回类型的推导:
auto add(int a, int b) {
return a + b; // 返回值的类型会自动推导为 int
}
register 存储类
register 用于提示编译器将变量存储在 CPU 寄存器中,而不是内存中,从而加快变量的访问速度。
但实际效果取决于编译器的优化策略和目标硬件,现代编译器通常会自行决定是否将变量存储在寄存器中,因此 register 关键字在现代 C++ 中已经不太常用了。
register int x = 5;
注意,register 变量不能取地址:
int* p = &x; // 错误:不能获取 register 变量的地址
static 存储类
static 关键字有两个主要用途:作用域内的静态变量以及全局/函数静态变量。它可以用于局部变量、全局变量或成员函数。
局部静态变量
局部变量默认在函数调用结束后被销毁,但使用 static 关键字修饰的局部变量会在函数调用之间保持其值,即使函数结束后它依然存在。
void counter() {
static int count = 0; // 静态局部变量,初始化一次
count++;
std::cout << count << std::endl;
}
int main() {
counter(); // 输出 1
counter(); // 输出 2
counter(); // 输出 3
return 0;
}
全局静态变量
全局变量或函数如果被 static 修饰,则它只能在当前文件中访问,不能被其他文件中的代码引用。
static int globalVar = 100; // 只在当前文件中可见
静态成员函数
静态成员函数属于类而不是对象,因此它不能访问类的非静态成员。静态成员函数可以通过类名直接调用。
class MyClass {
public:
static int x;
static void printX() {
std::cout << x << std::endl;
}
};
int MyClass::x = 5;
int main() {
MyClass::printX(); // 输出 5
return 0;
}
extern 存储类
extern 用于声明一个变量或函数在其他地方定义。这通常用于多文件项目中的变量共享或函数声明。
// file1.cpp
extern int x; // 声明 x 是外部变量
int main() {
x = 10; // 可以在这里使用 x
return 0;
}
// file2.cpp
int x = 10; // 定义 x
extern 也可以用于函数声明:
// file1.cpp
extern void foo(); // 声明 foo 函数在其他文件中定义
// file2.cpp
void foo() {
std::cout << "Hello, World!" << std::endl;
}
mutable 存储类
mutable 关键字允许在 const 成员函数中修改某些成员变量,即使对象本身是 const。
class MyClass {
public:
mutable int count; // 可以在 const 成员函数中修改
MyClass() : count(0) {}
void increment() const {
count++; // 修改 const 对象的 mutable 成员
}
};
int main() {
const MyClass obj;
obj.increment(); // 修改 count,即使 obj 是 const
std::cout << obj.count << std::endl; // 输出 1
return 0;
}
thread_local 存储类
thread_local 关键字指示变量在每个线程中都有一个独立的副本。当多个线程访问同一个 thread_local 变量时,每个线程都会持有该变量的不同副本,而不是共享一个变量。
#include <iostream>
#include <thread>
thread_local int count = 0; // 每个线程有一个独立的 count 变量
void increment() {
count++;
std::cout << "Count in thread: " << count << std::endl;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return 0;
}
存储类总结
(此处总结源自runnoob)
auto:这是默认的存储类说明符,通常可以省略不写。auto 指定的变量具有自动存储期,即它们的生命周期仅限于定义它们的块(block)。auto 变量通常在栈上分配。
register:用于建议编译器将变量存储在CPU寄存器中以提高访问速度。在 C++11 及以后的版本中,register 已经是一个废弃的特性,不再具有实际作用。
static:用于定义具有静态存储期的变量或函数,它们的生命周期贯穿整个程序的运行期。在函数内部,static变量的值在函数调用之间保持不变。在文件内部或全局作用域,static变量具有内部链接,只能在定义它们的文件中访问。
extern:用于声明具有外部链接的变量或函数,它们可以在多个文件之间共享。默认情况下,全局变量和函数具有 extern 存储类。在一个文件中使用extern声明另一个文件中定义的全局变量或函数,可以实现跨文件共享。
mutable (C++11):用于修饰类中的成员变量,允许在const成员函数中修改这些变量的值。通常用于缓存或计数器等需要在const上下文中修改的数据。
thread_local (C++11):用于定义具有线程局部存储期的变量,每个线程都有自己的独立副本。线程局部变量的生命周期与线程的生命周期相同。