C++ 学习笔记
一、auto
通过 auto
关键字声明的变量会在声明时自动进行类型推导,这个类型推导发生在编译过程中,因此不会影响运行效率。
auto
主要用于代替冗长复杂的变量声明,在很多情况下可以简化代码,但也会降低程序的可读性。
#include <iostream>
#include <map>
using namespace std;
int main(int argc, char *argv[]) {
map<int, string> languages;
languages[0] = "C++";
auto iter = languages.find(0); // 等价于 map<int, string>::iterator iter = languages.find(0);
if (iter != languages.end()) {
cout << iter->first << ":" << iter->second << endl; // 0:C++
}
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
0:C++
atreus@MacBook-Pro %
auto
基于模版类型推断实现,二者唯一的区别就是对统一初始化式的不同处理,使用统一初始化式对 auto
变量初始化会将其推断为 std::initializer_list
,但是模板类型推断无法完成推断。
template<typename T>
void func(T param) {
std::cout << param << std::endl;
}
template<typename T>
void func_(std::initializer_list<T> initList) {
std::cout << initList.size() << std::endl;
}
int main() {
auto x = {1, 2, 3}; // x->std::initializer_list<int>
func({1, 2, 3}); // 无法推断T的类型
func_({1, 2, 3}); // T->int,initList->std::initializer_list<int>
}
二、string
https://zh.cppreference.com/w/cpp/string/basic_string
C++98通过添加string类扩展了C++库,因此既可以用C风格字符串也可以用string类型来表示字符串。但string类使用起来比数组简单,同时提供了将字符串作为一种数据类型的表示方法。
如果采用C风格字符串,有两种表示形式,一种是 char a[] = {'a', 'b','v', '\0'};
需要手动添加结束符 '\0'
,另一种是 char a[] = "abv";
结尾会自动添加 '\0'
。同时注意区分C风格字符串和字符数组,字符数组可以表示字符串,但不代表二者等价:字符串后面必须有 '\0'
,字符数组不一定,但用字符数组定义字符串的时候,数组最后一个元素必须为 '\0'
。
与vector等容器类似,string类也提供了size和capacity两种属性。
- size:大小,即string类中实际存放了多少个字符。
- capacity:容量,即系统为当前string类分配的空间所能容纳的最大字符数,但不一定被填满。
对应的,有 resize()
和 reserve()
两种方法:
void resize(size_type count, const value_type &value);
:将字符串的尺寸设置为count,如果value被指定,则新创建的元素都将被初始化为value。void reserve(size_type new_cap);
:字符串的容量设置至少为new_cap,如果new_cap小于当前字符串中的字符数,容量将被设置为可以恰好容纳字符的数值。
#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;
int main(int argc, char *argv[]) {
string s = "Hello World!";
cout << s.size() << endl; // 12
cout << s.max_size() << endl; // 18446744073709551599
cout << s.capacity() << endl; // 22
s.resize(15, 97);
cout << s << endl; // Hello World!aaa
return 0;
}
三、new/delete 和 malloc/free
1.new 与 malloc
new
与 malloc
一样,都是用来动态分配内存,二者主要区别如下:
new | malloc | |
---|---|---|
分配区域 | 在自由存储区上进行内存空间的分配,自由存储区默认是堆,但也可以是静态存储区等其他内存区域 | 在堆上动态分配内存 |
分配大小 | 申请内存分配时无须指定内存块的大小,会按照指定数据类型自动进行分配 | 分配内存按照指定字节数进行分配 |
分配成功 | 内存分配成功时,返回的是指定类型的指针,无需进行类型转换 | 内存分配成功,返回的是 void * 类型的指针,需要强制类型转换为我们所需要的类型 |
分配失败 | 内存分配失败时会抛出一个bac_alloc异常而不会返回NULL,需要通过捕获异常来判断内存分配是否成功 | 内存分配失败时会直接返回NULL |
构造与析构操作 | 内存分配成功后通过 delete 来销毁内存,new 和 delete 会自动调用对象的构造函数/析构函数以完成对象的构造/析构 | 内存分配成功后需要通过 free 来销毁内存 |
初始化操作 | 分配内存空间的同时可以实现对象的初始化 | 无法实现初始化 |
/* 普通变量 */
int *p = new int(5);
delete p;
/* 一维数组 */
int *p_a = new int[5]{1, 2, 3, 4, 5};
delete[] p_a;
/* 二维数组 */
int **p_m = new int *[5];
for (int i = 0; i < 5; i++) {
p_m[i] = new int[5];
}
for (int i = 0; i < 5; i++) {
delete[] p_m[i];
}
delete[] p_m;
2.delete 与 delete[]
delete
与 delete[]
均可用于释放 new
所分配的内存,其中 delete
释放的一般是 new
分配的单个对象指针指向的内存,而 delete[]
释放一般是 new 类型[]
分配的数组指针指向的内存。
通过以下代码的执行结果可以发现,对于简单的基本数据类型,二者都能够成功地将内存释放(因为内存大小在分配时就已确定且不需要调用析构函数,不过并不推荐用 delete
释放数组),但是对于一些自定义数据类型的数组比如对象数组,使用 delete
时只会调用数组中首个对象的析构函数,其余元素的析构函数都无法执行,这对于一些通过析构函数释放文件描述符等系统资源的对象来说可能会导致系统资源的耗尽。
#include <iostream>
using namespace std;
class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};
int main(int argc, char *argv[]) {
int *p_i_arr = new int[3];
delete p_i_arr;
A *p = new A[3];
delete[] p;
A *p_arr = new A[3];
delete p_arr;
return 0;
}
atreus@MacBook-Pro % g++ -w main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
A()
A()
A()
~A()
~A()
~A()
A()
A()
A()
~A()
main(82556,0x1e6558140) malloc: *** error for object 0x600002cc8030: pointer being freed was not allocated
main(82556,0x1e6558140) malloc: *** set a breakpoint in malloc_error_break to debug
zsh: abort ./main
atreus@MacBook-Pro %
总之,使用 new
和 delete
时,应遵守以下规则:
- 不要使用
delete
来释放不是new
分配的内存; - 不要使用
delete
释放同一个内存块两次; - 如果使用
new 类型[]
为数组分配内存,则应使用delete[]
来释放; - 对空指针应用
delete
是安全的。
四、内联函数
使用 inline
关键字声明或定义的函数被称为内联函数。一般情况下编译器会用内联函数的代码直接替换函数调用(内联展开),这样就省去了函数调用时的跳转以及返回等操作,因此内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存(大多数情况下如此,但如果内联函数相当简单,当执行内联函数的指令少于函数调用的指令时,在提高了执行速度的同时也能节省内存)。
一般情况下,在类定义中的定义的函数都是内联函数,而在类外定义是需要通过 inline
关键字显式指定。而且程序员请求将函数作为内联函数时,编译器并不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归,但递归只是会导致 inline
失效,不属于语法错误),也有部分编译器没有启用或实现这种特性。
#include <iostream>
using namespace std;
inline int getMax(int a, int b) {
return a > b ? a : b;
}
int main() {
cout << getMax(1, 2) << endl;
return 0;
}
五、引用
1.引用的使用
引用是一个变量的别名,也就是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名或变量名来操作该变量,操作引用就相当于在操作引用所绑定的变量。
引用本质上相当于一个常指针:
int &a = b;
int *const a = &b;
引用与指针主要有以下区别:
- 指针是一个变量,存储的是变量的内存地址,引用是变量的别名。
- 指针定义时可以不初始化,引用定义时必须初始化。
- 指针定义时可以初始化为NULL,引用不能初始化为NULL。
- 指针有顶层const和底层const之分,引用只有常引用和普通引用。
- 非常指针在指针赋值后可以改变指针值,引用在初始化后不能再作为别的变量的别名。
sizeof
运算符作用于指针得到的是指针变量自身的大小,作用于引用得到的是引用所指向的变量的大小。- 指针可以有多级,引用只有一级。
- 指针的自增或自减表示指向下一个或上一个同类型变量的地址,一般用于指向数组的指针,引用的自增或自减表示指向变量值的增或减。
#include <iostream>
void swap(int &a, int &b) {
a = a + b;
b = a - b;
a = a - b;
}
int main (int argc, char *argv[]) {
int x = 0, y = 1;
swap(x, y);
std::cout << "x = " << x << ", y = " << y << std::endl;
return 0;
}
atreus@MacBook-Pro % g++ -w main.cpp -o main -std=c++11
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
x = 1, y = 0
atreus@MacBook-Pro %
此外,如果想通过引用提高程序的运行效率,同时保护传入值避免意外修改,可以以 const 类型 & 引用名
的形式传入一个常引用。
引用也可以作为函数的返回值,引用作为函数的返回值时,不能返回局部变量、局部对象、局部数组的引用,但可以返回一个全局变量或者静态变量的引用,主要有以下四种使用方式:
- 不接收返回值;
- 用一个普通变量接收函数的返回值,这时接收的是变量的值而不是变量的引用;
- 用一个引用来接收函数的返回值,接收的是一个引用;
- 当成左值来使用。
#include <iostream>
using namespace std;
int num = 1;
int &fun() {
return num;
}
int main(int argc, char const *argv[]) {
/* 不接收返回值 */
fun();
/* 用一个普通变量接收返回的引用 */
int a = fun();
cout << a << endl; // 1
a = 10;
cout << num << endl; // 1
/* 用一个引用去接收函数的返回值 接收的就是一个引用 */
int &b = fun();
cout << b << endl; // 1
b = 10;
cout << num << endl; // 10
/* 当成左值使用 */
fun() = 100;
cout << a << endl; // 10
cout << b << endl; // 100
cout << num << endl; // 100
return 0;
}
2.左值引用和右值引用
六、函数重载、隐藏、覆盖、重写
七、static
static
关键字主要有以下作用:
- 修饰局部变量或局部对象:可以延长该变量或对象的生命周期。
- 修饰全局变量、全局对象及普通函数:该变量只能在本文件中访问而不能在其他文件中访问。
- 修饰成员变量:该变量为静态成员变量,属于类而不属于实例化后的对象,与其他静态变量存储在一起,不占用对象的空间,但可以被所有实例化后的对象共享访问。需要类内定义,类外初始化,初始化时会为其分配内存。
- 修饰成员函数:该函数为静态成员函数,属于类而不属于实例化后的对象,无法通过 this 指针访问,函数中不能访问类的非静态成员变量及非静态成员函数。
八、const/constexpr 和 #define
const
修饰变量时该变量是一个真正的常量,而不是 C 中的常变量,const
常量会被编译器放入到符号表中,默认情况下不占用内存,但以下情况除外:
- 当我们对一个常量进行取地址时,操作系统会为该常量分配一段内存,并且用这个常量来填充。
- 当我们用一个变量为一个常量进行赋值时,这个常量也不会被放入符号表。
- 当我们用
const
修饰自定义数据类型(结构体、对象)和数组时系统也会分配空间。
除此以外,const
修饰的变量默认为内部链接性,因此当需要某个 const
变量的链接性为外部时,需要指定 const
变量为 extern
,即以 extern const
的形式定义此变量。
const
修饰成员函数,该函数只能访问而不能修改成员变量,如果需要修改时,需要用 mutable
修饰该变量。
const
修饰对象,称之为常对象,只能调用 const
修饰的成员函数。
const
并未区分出编译期常量和运行期常量,所以在 C++11 以后可以通过 constexpr
关键字定义编译期常量来提高运行效率,constexpr
将常量用宏来实现,但没有额外开销,更安全可靠。
const
和 #define
的区别:
#define
是在预处理阶段被处理的,无法调试,const
是在编译、运行阶段起作用。#define
只是简单的字符串替换,存在边界效应,不做任何类型检查,const
常量有具体的数据类型,在编译阶段会执行类型检查。#define
不需要内存分配,但是const
常量有可能存在内存分配。
九、构造函数和析构函数
1.构造函数与析构函数
构造函数是一种特殊的类成员函数,在创建时被调用。构造函数的名称和类名相同,没有返回类型(甚至连void都不是),但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标都不同。
当对象被删除时,程序将调用析构函数。析构函数没有返回类型、没有参数且无法重载,也因此每个类都只能有一个析构函数。
2.拷贝构造函数
拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,且其一个参数为本类型的一个引用变量。
拷贝构造函数主要在以下三种情况下调用:
- 对象以值传递的方式传入函数,如
fun(A)
。 - 对象以值传递的方式从函数返回,如
return A
。 - 对象需要通过另外一个对象进行初始化,如
Class A(B)
或Class A = B
。
拷贝构造函数还存在浅拷贝和深拷贝的问题,浅拷贝和深拷贝都可以实现对对象的复制,在不涉及指针等内存分配问题时二者并无区别。但如果类中存在动态内存分配,浅拷贝只会简单复制指针,这就导致析构函数执行时会连续delete相同的一段内存两次,而深拷贝会重新申请一段独立的内存空间以避免内存泄漏。
#include <iostream>
using namespace std;
class Student {
public:
int m_id;
char *m_name;
Student(int id, const char *name) {
this->m_id = id;
this->m_name = new char[10];
strcpy(this->m_name, name);
}
Student(const Student &other) {
this->m_id = other.m_id;
/* 浅拷贝 */
// this->m_name = other.m_name;
/* 深拷贝 */
this->m_name = new char[10];
strcpy(this->m_name, other.m_name);
}
~Student() {
delete[]this->m_name;
this->m_name = nullptr;
}
};
int main(int argc, char *argv[]) {
Student student(100, "rakan");
Student student_(student);
printf("%p\n", student.m_name);
printf("%p\n", student_.m_name);
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
0x6000033f0040
0x6000033f0050
atreus@MacBook-Pro %
3.移动构造函数
4.成员初始化器列表
在C++中,对象的成员变量的初始化动作发生在进入构造函数本体之前。在执行构造函数的函数体之前会首先调用该成员变量的默认构造函数(所以下例中尽管类C的构造函数未对m_b成员进行操作,但是m_b的构造函数仍然被执行),在进入函数体之后才会调用该成员变量指定的构造函数(所以下例中m_b总共执行了两次构造函数),因此,如果我们使用成员初始化器列表可以减少调用构造函数的过程,提高程序的效率。
就算不考虑效率,以下三种对象成员变量只能通过成员初始化器列表初始化:
- 常量成员,即const成员变量,因为常量成员只能初始化不能进行赋值。
- 引用成员,因为引用也必须在定义的时候初始化,并且不能重新赋值。
- 没有默认构造函数的类成员,因为使用成员初始化器列表初始化可以避免在执行构造函数函数体之前自动调用默认构造函数。
#include <iostream>
using namespace std;
class A {
public:
int m_data;
explicit A(int data) {
cout << "A(int data)" << endl;
this->m_data = data;
}
};
class B {
public:
int m_data;
B() {
cout << "B()" << endl;
}
explicit B(int data) {
cout << "B(int data)" << endl;
this->m_data = data;
}
};
class C {
private:
const int m_c_data; // 常量成员
int &m_r_data; // 引用成员
A m_a; // 没有默认构造函数的类成员
B m_b; // 有默认构造函数的类成员
public:
C(int c_data, int &r_data) : m_c_data(c_data), m_r_data(r_data), m_a(3) {
cout << "C(int c_data, int &r_data)" << endl;
m_b = B(4);
}
void show() const {
cout << "m_c_data : " << this->m_c_data << endl;
cout << "m_r_data : " << this->m_r_data << endl;
cout << "m_a.m_data : " << this->m_a.m_data << endl;
cout << "m_b.m_data : " << this->m_b.m_data << endl;
}
};
int main(int argc, char *argv[]) {
int c_data = 1, r_data = 2;
C c(c_data, r_data);
c.show();
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
A(int data)
B()
C(int c_data, int &r_data)
B(int data)
m_c_data : 1
m_r_data : 2
m_a.m_data : 3
m_b.m_data : 4
atreus@MacBook-Pro %
与成员初始化器列表类似,还可以在声明成员变量时就为其定义初始化方法,这也被称为类内初始化。
class Student {
const int m_c_data = 0; // 常量成员
int &m_r_data = x; // 引用成员
A m_a = A(0); // 没有默认构造函数的类成员
B m_b = B(0); // 有默认构造函数的类成员
};
5.默认成员函数
对于一个类,不对其作任何操作,以下八个函数会在需要时由编译器默认生成,他们都是inline且public的:
A(); // 1.默认构造函数
~A(); // 2.默认析构函数
A(const A &); // 3.默认拷贝构造函数
A &operator=(const A &); // 4.默认赋值运算符重载
A *operator&(); // 5.默认取地址运算符重载
const A *operator&() const; // 6.默认取址运算符const重载
A(const A &&); // 7.默认移动构造函数(C++11)
A &operator=(const A &&); // 8.默认移动赋值运算符重载(C++11)
6. =default 和 =delete
正如上面所说,这些方法只有在程序员没有定义同类方法时才会生成,假如我们定义了一个有参构造函数,那么如果想使用默认的无参构造函数就需要显式定义(其他七个方法也是类似)。为了方便这种定义,C++11中提出了default关键字,我们通过 = defalut
即可定义一个默认无参构造函数。
与此类似,= delete
用于禁止编译器使用相关方法。例如:对于单例模式来说,我们通常需要将单例类的构造函数私有(private)以避免其显示实例化,但现在我们完全可以通过 = delete
来实现同样的目的,且更不容易犯错,更容易理解。
= defalut
和 = delete
还有一个区别,那就是 = delete
可以用于任何成员函数并将其禁用,但 = defalut
仅能用于以下六个存在默认选项的成员函数。
class A {
public:
A() = default;
~A() = default;
A(const A &) = default;
A &operator=(const A &) = default;
A(A &&) = default;
A &operator=(A &&) = default;
};
十、继承和多态
十一、Lambda 表达式
Lambda表达式实际是匿名函数,能够捕获一定范围的变量。与普通函数不用,Lambda表达式可以在函数内部定义,其原型为:[捕获列表](参数列表) mutable -> 返回类型 {函数体};
。
Lambda表达式本质上是一个匿名函数对象,它的底层实现主要分为以下三步:
- 创建Lambda匿名类,实现构造函数,同时重载
operator()
运算符,所以Lambda表达式也叫匿名函数对象。 - 实例化Lambda对象。
- 通过对象调用
operator()
。
也正是因此,Lambda表达式可以直接读取 const
修饰的常量而无需捕获。
在实际使用中,Lambda函数主要由五部分组成:
4. [捕获列表]
:捕获列表总是出现在Lambda函数的开始处。实际上,[]
是Lambda引出符,编译器根据该引出符判断接下来的代码是否是Lambda函数,同时捕获列表能够捕获上下文中的变量以供Lambda函数使用。
5. (参数列表)
:与普通函数的参数列表一致。如果不需要参数传递,则可以连同括号 ()
一起省略。
6. mutable
:默认情况下,Lambda函数是一个 const
函数,mutable
可以取消其常量性,在使用该修饰符时,即使参数为空参数列表也不能省略。
7. -> 返回类型
:除了在不需要返回值的时候可以将返回类型连同 ->
一起省略,在返回类型明确的情况下,也可以省略该部分,由编译器来对返回类型进行推导。
8. {函数体}
:内容与普通函数一样,可以使用所有捕获的变量。
捕获列表的捕获规则:
格式 | 含义 |
---|---|
[] | 不捕获任何变量,但不包括静态局部变量 |
[&] | 捕获外部作用域中所有变量,并作为引用在函数体里使用 |
[=] | 捕获外部作用域中的所有变量并作为副本在函数体里使用,但不能修改外部作用域变量的值 |
[this] | 一般用于类中,捕获当前类中的this指针,让lanbda表达式与成员函数具有一样的权限访问成员变量 |
[变量名] | 按值捕获列表中列出的变量,变量间由逗号分隔 |
[=, &变量名] | 按值捕获外部变量,按引用捕获指定变量 |
[&, 变量名] | 按引用捕获外部变量,按值捕获指定变量 |
#include <iostream>
using namespace std;
int main() {
auto fun = [](int num = 0) {
cout << "Hello world!" << endl;
return num;
};
cout << fun(1) << endl;
return 0;
}
atreus@AtreusdeMacBook-Pro % clang++ main.cpp -o main -std=c++11
atreus@AtreusdeMacBook-Pro % ./main
Hello world!
1
atreus@AtreusdeMacBook-Pro %
#include <iostream>
using namespace std;
int main() {
int num1 = 1;
int num2 = 1;
/* [&] 按照引用捕获 可以修改外部的值 */
auto fun_ref = [&]() {
num1++;
};
/* [=] 按值捕获 只能使用不能修改外部的值 */
auto fun_val = [=]() {
num1++; // cannot assign to a variable captured by copy in a non-mutable lambda
};
/* [(=/&), (=/&)变量名, (=/&)变量名, ···] 按照指定要求捕获指定变量 */
auto fun_spe = [=, &num1] {
num1++;
num2++; // cannot assign to a variable captured by copy in a non-mutable lambda
};
fun_ref();
fun_val();
fun_spe();
return 0;
}
十二、智能指针和 RAII 机制
十三、STL 容器
十四、RTTI 和类型转换运算符
1.typeid 与 type_info
RTTI是运行阶段类型识别(Runtime Type Identification)的简称,C++中有 typeid
、type_info
和 dynamic_cast
三个支持RTTI的元素。
typeid
运算符可以用于确定两个对象是否为同种类型,它可以接受类名和结果为对象的表达式这两种参数并返回一个对 type_info
对象的引用。
#include <iostream>
#include <vector>
using namespace std;
class A {};
class B : public A {};
int main() {
int case_int;
string case_string;
vector<int> case_vector;
A case_A;
B case_B;
const type_info &info_int = typeid(case_int);
cout << info_int.name() << endl;
const type_info &info_string = typeid(case_string);
cout << info_string.name() << endl;
const type_info &info_vector = typeid(case_vector);
cout << info_vector.name() << endl;
const type_info &info_A = typeid(case_A);
cout << info_A.name() << endl;
const type_info &info_B = typeid(case_B);
cout << info_B.name() << endl;
if (typeid(A) == typeid(case_A)) {
cout << "typeid(A) == typeid(case_A)" << endl;
} else {
cout << "typeid(A) != typeid(case_A)" << endl;
}
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
i
NSt3__112basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEE
NSt3__16vectorIiNS_9allocatorIiEEEE
1A
1B
typeid(A) == typeid(case_A)
atreus@MacBook-Pro %
2.强制类型转换运算符
static_cast
static_cast<新类型> (表达式)
用隐式和用户定义转换的组合在类型间转换。它跟传统的隐式转换方式几乎是一致的,编译器隐式执行的任何类型转换都可用 static_cast
,但它不能用于两个不相关的类型进行转换 。
#include <iostream>
class Car {};
class Volvo : public Car {};
class Train {};
int main() {
Volvo *volvo = nullptr;
Car *car;
Train *train;
car = static_cast<Car *> (volvo);
volvo = static_cast<Volvo *> (car);
train = static_cast<Train *> (car);
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
main.cpp:14:13: error: static_cast from 'Car *' to 'Train *', which are not related by inheritance, is not allowed
train = static_cast<Train *> (car);
^~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.
atreus@MacBook-Pro %
reinterpret_cast
reinterpret_cast<新类型> (表达式)
通过重新解释底层位模式在类型间转换(即逐个比特复制)。它用于执行很多天生危险的类型转换操作,同时非常灵活,但转换的安全性需要程序员来保证,类似于C语言中的强制类型转换。当然 reinterpret_cast
也不是支持所有的类型转换,比如不能将指针转换为字节数更小的整形或浮点型,也不能实现函数指针和数据指针的相互转换。
#include <iostream>
int main() {
int x = 65;
int *p = &x;
char *c = reinterpret_cast<char *> (p);
std::cout << *c << std::endl; // A
return 0;
}
const_cast
const_cast<新类型> (表达式)
在有不同的 const
和 volatile
限定的类型间转换,即用于改变类型中的 const
和 volatile
修饰,因此除了 const
和 volatile
特征,转换前后其他的类型特征应当完全一致。
#include <iostream>
using namespace std;
class Car {};
int main() {
const Car *car = nullptr;
auto *car1 = const_cast<Car *> (car);
auto *car2 = const_cast<const Car *> (car1);
if (typeid(car) == typeid(Car *)) {
cout << "car: Car *" << endl;
} else if (typeid(car) == typeid(const Car *)) {
cout << "car: const Car *" << endl;
};
if (typeid(car1) == typeid(Car *)) {
cout << "car1: Car *" << endl;
} else if (typeid(car1) == typeid(const Car *)) {
cout << "car1: const Car *" << endl;
};
if (typeid(car2) == typeid(Car *)) {
cout << "car2: Car *" << endl;
} else if (typeid(car2) == typeid(const Car *)) {
cout << "car2: const Car *" << endl;
};
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
car: const Car *
car1: Car *
car2: const Car *
atreus@MacBook-Pro %
dynamic_cast
dynamic_cast<新类型> (表达式)
沿继承层级向上、向下及侧向,安全地转换到其他类的指针和引用。它专门用于将多态基类的指针或引用强制转换为派生类的指针或引用(因为向上转型可以隐式实现),而且能够检查转换的安全性。
static_cast
也可用于相关类之间的类型转换,二者主要有以下两点不同:
- 对于存在多态的相关类的向下转换,
static_cast
会进行无条件的转换,而向下转型往往是存在问题的,但static_cast
无法检测出任何问题。dynamic_cast
通过RTTI来保证类型转换的安全性,对于不安全的指针转换,转换结果返回NULL指针,对于不安全的引用转换,抛出一个bad_cast异常。 - 对于无关类的转换,
dynamic_cast
是在运行时检错,而static_cast
会在编译期报错。
#include <iostream>
using namespace std;
class Base {
public:
virtual void func() {};
};
class Derive : public Base {};
class Other {
public:
void func_() {
cout << "1" << endl;
}
};
int main() {
auto *base = new Base;
auto *derive1 = dynamic_cast<Derive *> (base);
auto *derive2 = static_cast<Derive *> (base);
if (derive1 == NULL) {
cout << "dynamic_cast<Derive *> (base) unsafe" << endl;
}
auto *other1 = dynamic_cast<Other *> (base);
auto *other2 = static_cast<Other *> (base); // error: static_cast from 'Base *' to 'Other *', which are not related by inheritance, is not allowed
if (other1 == NULL) {
cout << "dynamic_cast<Other *> (base) unsafe" << endl;
}
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main -std=c++11
atreus@MacBook-Pro % ./main
dynamic_cast<Derive *> (base) unsafe
dynamic_cast<Other *> (base) unsafe
atreus@MacBook-Pro %
十五、this
this
实际上是成员函数的一个形参,在调用成员函数时对象的地址会被作为实参传递给 this
。不过 this
这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。this
作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才会给 this
赋值。
实际上,成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。这个额外的参数实际上就是 this
,它是成员函数和成员变量关联的桥梁。
this
指针作为类成员函数的一个隐含参数,不需要人为操作,因此它与其它类的成员不同,不存于栈中,而是存储在在寄存器里。this
指针可以为空,但有条件,当我们要进行的操作不需要 this
指针去指向某个对象,例如仅仅是打印一些与类无关信息的时候就可以令它为空。
十六、模板
泛型编程以一种独立于任何特定类型的方式编写代码,模板则是泛型编程的基础,它是创建泛型类或函数的蓝图或公式。
1.函数模板
函数模板是通用的函数描述,它们使用泛型来定义函数,其中泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。函数模板具有与函数重载相似的效果,但实现起来更方便。
#include <iostream>
using namespace std;
template<typename T, typename U>
void show(T a, U b) {
cout << "T: " << a << " " << "U: " << b << endl;
}
int main() {
show(1, 1);
show(0.1, 0.1);
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main
atreus@MacBook-Pro % ./main
T: 1 U: 1
T: 0.1 U: 0.1
atreus@MacBook-Pro %
2.类模板
通过类模板实现一个对多个类型适用的数组类:
#include <iostream>
#include <vector>
#define MAX_SIZE 1024
using namespace std;
template <typename T>
class Array {
private:
int len;
T data[MAX_SIZE];
public:
Array() {
len = 0;
memset(data, 0, sizeof(data));
}
void add(T element) {
data[len] = element;
len++;
}
void show() {
for (int i = 0; i < len; i++) {
cout << data[i] << " ";
}
cout << endl;
}
};
int main() {
Array<int> array_int;
array_int.add(1);
array_int.add(2);
array_int.add(3);
array_int.show(); // 1 2 3
Array<string> array_str;
array_str.add("a");
array_str.add("b");
array_str.add("c");
array_str.show(); // a b c
return 0;
}
十七、友元
友元的目的就是让一个函数或者类可以访问另一个类中的私有成员,包括友元函数和友元类:
- 将全局函数声明为友元函数:
friend 返回值类型 函数名(参数列表);
。 - 将其他类的成员函数声明为友元函数:
friend 返回值类型 其他类的类名::成员函数名(参数列表);
。 - 将其他类声明为友元类:
friend class 类名;
。
友元函数并不是类的成员函数,也不由对象调用,它使用的所有的值都是显式参数。
#include <iostream>
class Student {
private:
int m_age;
public:
explicit Student(int age) : m_age(age) {}
/* 将Group声明为友元类 */
friend class Group;
};
class Group {
private:
Student m_leader;
public:
explicit Group(const Student &leader) : m_leader(leader) {}
/* 通过友元函数重载流运算符 */
friend std::ostream &operator<<(std::ostream &os, Group group) {
os << group.m_leader.m_age; // Group为Student友元类,因此可以访问其私有成员
return os;
}
};
int main() {
Student student(10);
Group group(student);
std::cout << group << std::endl; // 10
return 0;
}
十八、运算符重载
运算符重载允许把 +
、-
、*
、/
等标准运算符应用于自定义数据类型对象。运算符重载本质上是函数重载,只是函数调用的一种方式,直观自然,可以提高程序的可读性。
运算符重载可以通过成员函数重载或者友元函数重载实现,注意事项如下:
- 重载后的运算符必须至少有一个操作数是用户定义的类型。
- 运算符重载只能重载已有的运算符,即不能创建新的运算符。
- 运算符重载不能改变运算符操作对象的个数、优先级以及结合性,即不能违反运算符原来的句法规则。
- 运算符
::
、?:
、.
、.*
、sizeof
、typeid
、const_cast
、dynamic_cast
、reinterpret_cast
、static_cast
不允许重载。 - 双目运算符
=
、()
、[]
、->
与类型转换运算符只能以成员函数方式重载,流运算符<<
与>>
只能以友元函数的方式重载。 - 对于
++
和--
运算符,前置时作为一元运算符进行重载,后置时作为二元运算符进行重载,其中前置重载时相对来说性能开销更小。 - 在重载
+
、-
、*
、/
等二元运算符时,为了保证与其他类型计算时顺序的任意性,最好通过友元函数重载。
#include <iostream>
using namespace std;
class Goods {
private:
double m_price;
public:
explicit Goods(double price) : m_price(price) {}
Goods operator+(const Goods &g) const; // 通过成员函数重载+,此时只支持Goods对象相加
Goods operator+(int x) const; // 通过成员函数重载+,此时支持Good对象与int相加,但int必须位于+右侧
friend Goods operator+(int x, const Goods &g); // 通过友元函数重载+,此时支持Good对象与int相加,int可以位于+任意一侧
friend ostream &operator<<(ostream &out, const Goods &g); // 通过友元函数重载<<
Goods &operator++(); // 通过成员函数重载前置++
Goods operator++(int x); // 通过成员函数重载后置++
};
Goods Goods::operator+(const Goods &g) const {
cout << "Goods operator+(const Goods &g) const" << endl;
return Goods(this->m_price + g.m_price);
}
Goods Goods::operator+(int x) const {
cout << "Goods operator+(int x) const" << endl;
return Goods(this->m_price + x);
}
Goods operator+(int x, const Goods &g) {
cout << "friend Goods operator+(int x, const Goods &g)" << endl;
return Goods(g.m_price + x);
}
ostream &operator<<(ostream &out, const Goods &g) {
out << g.m_price;
return out;
}
Goods &Goods::operator++() {
m_price++;
return *this;
}
Goods Goods::operator++(int x) {
Goods tmp(*this);
m_price++;
return tmp;
}
int main() {
Goods goods(0);
goods = goods + goods;
goods = goods + 1;
goods = 1 + goods;
cout << goods << endl;
return 0;
}
atreus@MacBook-Pro % g++ main.cpp -o main
atreus@MacBook-Pro % ./main
Goods operator+(const Goods &g) const
Goods operator+(int x) const
friend Goods operator+(int x, const Goods &g)
2
atreus@MacBook-Pro %