一、基础入门
1、常量
宏常量 #define
常量 const
#include<>
// 宏常量
#define DAY 7
int main() {
// const常量
const int month = 12;
return 0;
}
面试题:宏常量与const常量有什么区别?
- const常量有数据类型,在编译阶段进行类型检查,而宏常量没有数据类型,也就不存在类型检查
- const常量在运行阶段进行替换,而宏常量在预编译阶段进行替换;
2、关键字
asm | do | if | return | typedef |
---|---|---|---|---|
auto | double | inline | short | typeid |
bool | dynamic_cast | int | signed | typename |
break | else | long | sizeof | union |
case | enum | mutable | static | unsigned |
catch | explicit | namespace | static_cast | using |
char | export | new | struct | virtual |
class | extern | operator | switch | void |
const | false | private | template | volatile |
const_cast | float | protected | this | wchar_t |
continue | for | public | throw | while |
default | friend | register | true | |
delete | goto | reinterpret_cast | try |
3、数据类型
定义各种数据类型是为了给变量分配合适的内存空间,避免造成资源浪费。sizeof(DataType)用于计算数据类型所占内存大小。
3.1、整型
数据类型 | 占用空间 | 取值范围 |
---|---|---|
short | 2字节 | (-2^15 ~ 2^15-1) |
int | 4字节 | (-2^31 ~ 2^31-1) |
long | Windows为4字节,Linux为4字节(32位),8字节(64位) | (-2^31 ~ 2^31-1) |
long long | 8字节 | (-2^63 ~ 2^63-1) |
3.2、浮点型
数据类型 | 占用空间 | 有效数字范围 |
---|---|---|
float | 4字节 | 7位有效数字 |
double | 8字节 | 15~16位有效数字 |
默认情况下,浮点数的输出只显示6位有效数字 |
float f = 3.14f; //默认小数位double,加上f表示为float型
double d = 3.14;
// 科学计数法3.14 * 10^-2 = 0.0314
float f2 = 3.14e-2;
3.3、字符型
C和C++中字符型变量char只占用1个字节,字符型变量并不是把字符本身放到内存中存储,而是将对应的ASCII编码放入到存储单元。
ASCII码表:
ASCII值 | 控制字符 | ASCII值 | 字符 | ASCII值 | 字符 | ASCII值 | 字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 32 | (space) | 64 | @ | 96 | 、 |
1 | SOH | 33 | ! | 65 | A | 97 | a |
2 | STX | 34 | " | 66 | B | 98 | b |
3 | ETX | 35 | # | 67 | C | 99 | c |
4 | EOT | 36 | $ | 68 | D | 100 | d |
5 | ENQ | 37 | % | 69 | E | 101 | e |
6 | ACK | 38 | & | 70 | F | 102 | f |
7 | BEL | 39 | , | 71 | G | 103 | g |
8 | BS | 40 | ( | 72 | H | 104 | h |
9 | HT | 41 | ) | 73 | I | 105 | i |
10 | LF | 42 | * | 74 | J | 106 | j |
11 | VT | 43 | + | 75 | K | 107 | k |
12 | FF | 44 | , | 76 | L | 108 | l |
13 | CR | 45 | - | 77 | M | 109 | m |
14 | SO | 46 | . | 78 | N | 110 | n |
15 | SI | 47 | / | 79 | O | 111 | o |
16 | DLE | 48 | 0 | 80 | P | 112 | p |
17 | DCI | 49 | 1 | 81 | Q | 113 | q |
18 | DC2 | 50 | 2 | 82 | R | 114 | r |
19 | DC3 | 51 | 3 | 83 | S | 115 | s |
20 | DC4 | 52 | 4 | 84 | T | 116 | t |
21 | NAK | 53 | 5 | 85 | U | 117 | u |
22 | SYN | 54 | 6 | 86 | V | 118 | v |
23 | TB | 55 | 7 | 87 | W | 119 | w |
24 | CAN | 56 | 8 | 88 | X | 120 | x |
25 | EM | 57 | 9 | 89 | Y | 121 | y |
26 | SUB | 58 | : | 90 | Z | 122 | z |
27 | ESC | 59 | ; | 91 | [ | 123 | { |
28 | FS | 60 | < | 92 | / | 124 | | |
29 | GS | 61 | = | 93 | ] | 125 | } |
30 | RS | 62 | > | 94 | ^ | 126 | ` |
31 | US | 63 | ? | 95 | _ | 127 | DEL |
转义字符:
转义字符 | 含义 | ASCII码值(十进制) |
---|---|---|
\a | 警报 | 007 |
\b | 退格(BS) ,将当前位置移到前一列 | 008 |
\f | 换页(FF),将当前位置移到下页开头 | 012 |
\n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
\r | 回车(CR) ,将当前位置移到本行开头 | 013 |
\t | 水平制表(HT) (跳到下一个TAB位置) | 009 |
\v | 垂直制表(VT) | 011 |
\\ | 代表一个反斜线字符"" | 092 |
’ | 代表一个单引号(撇号)字符 | 039 |
" | 代表一个双引号字符 | 034 |
? | 代表一个问号 | 063 |
\0 | 数字0 | 000 |
\ddd | 8进制转义字符,d范围0~7 | 3位8进制 |
\xhh | 16进制转义字符,h范围09,af,A~F | 3位16进制 |
3.4、字符串类型
- C风格字符串:
char 变量名[] = "字符串值"
- C++风格字符串:
string 变量名 = "字符串值"
注意:C++风格字符串,需要加入头文件#include<string>
#include<iostream>
#include<string>
using namespace std;
int main() {
char c1 = 'a';
// c风格的字符串
char str[] = "abc";
// c++风格的字符串 需要头文件#include<string>
string str2 = "abc";
cout << "char str[]" << str << endl;
cout << "string str2" << str2 << endl;
system("pause");
return 0;
}
3.5、布尔类型
bool类型只有两个值:true — 真(本质是1),false — 假(本质是0)
bool类型占1个字节大小
对bool类型进行赋值时,只要不为0,则为true
4、数据的输入与输出
#include<iostream>
#include<string>
using namespace std;
int main() {
// 对于cin的连续输入,是以空白字符为分隔符的
// (下一个变量从非空白字符开始)
int a, b;
cout << "输入a和b:";
cin >> a >> b;
char c, d;
cout << "输入c和d:";
cin >> c >> d;
string e, f;
cout << "输入e和f:";
cin >> e >> f;
cout << "a = " << a << ", b = " << b << endl;
cout << "c = " << c << ", d = " << d << endl;
cout << "e = " << e << ", f = " << f << endl;
// C++中的scanf_s()相当于C语言中的scanf()
// C++中的printf_s()相当于C语言中的printf()
// 当需要格式化输入或输出时,可考虑使用scanf_s()和printf_s
cout << "输入a和b:";
scanf_s("%d,%d", &a, &b);
printf_s("a = %d, b = %d\n", a, b);
return 0;
}
对于不确定输入次数的,可通过ctrl + z
结束输入
string str;
int index = 0;
// ctrl + Z 结束输入
while (cin >> str) {
cout << "index = " << ++index << "str = " << str << endl;
}
5、运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
+ | 正号 | +3 | 3 |
- | 负号 | -3 | -3 |
+ | 加 | 10 + 5 | 15 |
- | 减 | 10 - 5 | 5 |
* | 乘 | 10 * 5 | 50 |
/ | 除 | 10 / 5 | 2 |
% | 取模(取余) | 10 % 3 | 1 |
++ | 前置递增 | a=2; b=++a; | a=3; b=3; |
++ | 后置递增 | a=2; b=a++; | a=3; b=2; |
– | 前置递减 | a=2; b=–a; | a=1; b=1; |
– | 后置递减 | a=2; b=a–; | a=1; b=2; |
= | 赋值 | a=2; b=3; | a=2; b=3; |
+= | 加等于 | a=0; a+=2; | a=2; |
-= | 减等于 | a=5; a-=3; | a=2; |
*= | 乘等于 | a=2; a*=2; | a=4; |
/= | 除等于 | a=4; a/=2; | a=2; |
%= | 模等于 | a=3; a%2; | a=1; |
== | 相等于 | 4 == 3 | 0 |
!= | 不等于 | 4 != 3 | 1 |
< | 小于 | 4 < 3 | 0 |
> | 大于 | 4 > 3 | 1 |
<= | 小于等于 | 4 <= 3 | 0 |
>= | 大于等于 | 4 >= 1 | 1 |
! | 非 | !a | 如果a为假,则!a为真; 如果a为真,则!a为假。 |
&& | 与 | a && b | 如果a和b都为真,则结果为真,否则为假。 |
|| | 或 | a || b | 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。 |
两个整数相除只保留整数,只有整数可以进行取模运算
int ii1 = 0 % 10; //正确
int ii2 = 10 % 0; //错误
6、数组
6.1、一维数组
数据类型 数组名[ 数组长度 ];
数据类型 数组名[ 数组长度 ] = { 值1,值2 ...};
数据类型 数组名[ ] = { 值1,值2 ...};
- 一维数组名:
- 可以统计整个数组在内存中的长度
- 可以获取数组在内存中的首地址
- 数组名是常量,不可以赋值
6.2、二维数组
数据类型 数组名[ 行数 ][ 列数 ];
数据类型 数组名[ 行数 ][ 列数 ] = { {数据1,数据2 } ,{数据3,数据4 } };
数据类型 数组名[ 行数 ][ 列数 ] = { 数据1,数据2,数据3,数据4};
数据类型 数组名[ ][ 列数 ] = { 数据1,数据2,数据3,数据4};
- 二维数组名:
- 查看二维数组所占内存空间
- 获取二维数组首地址
7、函数
函数的声明可以多次,函数的定义只能一次
函数的分文件书写:
1)创建.h头文件,写函数声明,及程序需要的文件头
2)创建cpp原文件,写函数的定义
3)cpp引入头文件include “swap.h”
8、指针
在32位系统下,所有数据类型的指针都是占4字节
在64位系统下,所有数据类型的指针都是占8字节
空指针指向的地址为0,且无法访问,访问报错
指针定义时若没有初始化,则初始化为NULL
野指针访问出错
#include<iostream>
using namespace std;
// 引用传递/地址传递
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 10;
int b = 20;
int *p = &a;
int *p2 = NULL;
cout << "a的地址:" << &a << endl;
cout << "a的值:" << a << endl;
cout << "指针p:" << p << endl;
cout << "解引用*p = " << *p << endl;
cout << "指针类型的大小:" << sizeof(int *) << endl;
cout << "空指针p2:" << p2 << endl;
// 空指针指向的地址是0x0000,对空指针解引用运行出错(编译可通过)
//cout << "空指针p2解引用:" << *p2 << endl;
// 常量指针,指针指向的值不可改
const int *p3 = &a;
p3 = &b;
// 指针常量,指针的指向不可改
int *const p4 = &a;
*p4 = 30;
const int *const p5 = &a;
int arr[] = { 1, 2, 3, 4, 5 };
int *p6 = arr;
cout << "利用指针打印int数组:";
for (int i = 0; i < 5; i++) {
cout << *p6 << " ";
// 偏移一个int大小
p6 ++;
}
// 引用传递/地址传递
cout << "\na = " << a << ", b = " << b << endl;
swap(&a, &b);
cout << "a = " << a << ", b = " << b << endl;
system("pause");
return 0;
}
9、结构体
结构体作为函数参数时,使用结构体指针可以节省内存。
// 使用const可防止误改结构体中的值
void print(const Student *s) {
cout << s->name << " " << s->age << endl;
}
二、核心编程
1、内存分区
代码区(运行前):共享(一个程序的多个进程共享代码),只读(代码区不可修改)
全局区(运行前):该区域的数据在程序结束后由操作系统释放
栈区(运行后):函数不要返回局部变量的地址
2、引用
1、引用定义时必须初始化,且引用不可修改指向
2、若函数的返回值为引用,则可以作为左值进行赋值操作
3、引用的本质是指针常量int * const ref = &a
4、引用符号写到靠近数据类型的一边,而不是变量名
3、函数高级
1、函数的某个参数有默认值,则其之后的参数也必须有默认值
2、函数的声明与函数的定义的参数只能有一个有默认值
3、
4、函数重载
5、函数有默认值与函数重载可能出现二义性
7、引用作为重载条件
8、成员函数中const修饰符可作为重载函数
class A {
public:
void funcB() {
std::cout << "funcB without const" << std::endl;
}
void funcB() const {
std::cout << "funcB with const" << std::endl;
}
};
int main() {
A a1;
const A a2;
// 打印的是 funcB without const
a1.funcB();
// 打印的是 funcB with const
a2.funcB();
// 若函数funcB without const不存在,则a1.funcB()会打印funcB with const
// 若函数funcB with const不存在,则a2.funcB()编译不通过
// error: passing 'const A' as 'this' argument discards qualifiers [-fpermissive]
return 0;
}
4、struct与class的区别
C++中,struct默认权限为公有,而class默认属性为私有
5、防止头文件重复包含
6、构造函数与析构函数
C++默认提供无参构造函数,拷贝构造函数和无参析构函数。
若自定义了普通构造函数,则不再提供拷贝构造函数;
若自定义了拷贝构造函数,则不再提供任何构造函数。
拷贝构造函数调用的时机:
7、深拷贝与浅拷贝
浅拷贝:对于指针类型,只是将原对象的指针所指的地址值赋给了新对象,浅拷贝存在堆内存重复释放的问题(默认提供的拷贝构造函数就是浅拷贝)
深拷贝:对于指针类型,需要重新new一个堆内存,解决浅拷贝问题,需要自定义拷贝构造函数
8、类的特性
C++会给每个空对象分配1字节的内存空间,是为了区分空对象占内存的位置
当类只有一个非静态成员变量int时,只占4字节
成员变量和成员函数是分开存储的,只有非静态成员变量属于类对象上
9、this指针
this指针的本质是student * const this
,指针常量,不可修改指向
1、用于将成员变量与同名的成员函数形参区别开
2、用于成员函数的链式调用
3、类对象的空指针问题
10、常函数、常对象
1、常函数内的this指针是const student * const this
2、常函数中只能修改mutable修改的成员变量
3、常对象只能调用常函数
11、友元
全局函数做友元
12、运算符重载
成员函数重载和全局函数重载运算符的优先级?
不可重复定义
运算符重载函数也可发生函数重载
内置的数据类型不可以发生运算符重载
前置递增返回引用,后置递增返回值
对于带有指针变量的类,编译器自带的赋值运算符是浅拷贝,会出现堆内存重复释放的问题(同拷贝函数)
#include<iostream>
#include<string>
using namespace std;
class Person {
public:
int age;
int num;
int *tt;
Person(int age, int num):age(age),num(num){
this->tt = new int(10);
}
Person(const Person &p) {
cout << "拷贝构造函数" << endl;
this->age = p.age;
this->num = p.num;
// 对于有指针成员变量的类,需要重写赋值运算符和拷贝构造函数改为深拷贝
this->tt = new int(*p.tt);
}
~Person() {
if (this->tt != NULL) {
delete this->tt;
this->tt = NULL;
}
}
// 重载运算符+,定义两个Person对象相加的意义
Person operator-(Person &p) {
Person temp(this->age - p.age, this->num - p.num);
return temp;
}
// 运算符重载,定义加一个int型变量进行的操作
Person operator+(int a) {
Person temp(this->age + a, this->num + a);
return temp;
}
// 重载运算符--, 定义 --p
Person& operator--() {
this->num --;
return *this;
}
// 重载运算符--, 定义 p--, 占位参数int表示是后置操作
Person operator--(int) {
Person temp = *this;
this->num--;
return temp;
}
// 重载运算符=,定义 this = p,编译器默认提供浅拷贝
Person& operator=(const Person &p) {
cout << "赋值运算符" << endl;
this->age = p.age;
this->num = p.num;
// 对于有指针成员变量的类,需要重写赋值运算符和拷贝构造函数改为深拷贝
this->tt = new int(*p.tt);
return *this;
}
};
Person operator+(Person &p1, Person &p2) {
Person temp(p1.age + p2.age, p1.num + p2.num);
return temp;
}
// 重载运算符<<,定义 cout << p;,只能通过全局函数重载,无法通过成员函数重载
ostream& operator<<(ostream &cout, Person &p) {
cout << "age = " << p.age << " num = " << p.num;
// cout进行链式调用,需要返回ostream的引用
return cout;
}
// 重载运算符++,定义 ++p
// 前置操作返回的是引用
Person& operator++(Person &p) {
p.num ++;
return p;
}
// 重载运算符++,定义 p++, 多一个int占位参数,表示是后置操作
// 后置操作返回的是值
Person operator++(Person &p, int) {
Person temp = p;
p.num++;
return temp;
}
int main() {
Person p1(21, 1);
Person p2(20, 2);
Person p3 = p2 + p1;
cout << "p3: age = " << p3.age << " num = " << p3.num << endl;
Person p4 = p1 - p2;
cout << "p4: age = " << p4.age << " num = " << p4.num << endl;
Person p5 = p1 + 2;
cout << "p5: age = " << p5.age << " num = " << p5.num << endl;
cout << p5 << endl;
++(++p5);
p5++;
cout << p5 << endl;
--(--p5);
p5--;
cout << p5 << endl;
Person p6(0, 0);
p6 = p5;
cout << "p6: " << p6 << endl;
return 0;
}
13、继承
父类中所有的非静态成员都会被子类继承下去,虽然父类的私有成员在子类中访问不到。
继承中,父类先构造后析构,子类后构造先析构
父类和子类中存在同名成员变量/函数,直接访问的是子类的成员变量/函数,访问父类中的同名成员变量/函数需要加作用域,静态成员同理。
Son s;
s.Father::m_a;
s.Father::func();
如何子类中存在与父类同名的成员函数,则父类中所有的同名成员函数都会被隐藏,尽管父类中的同名成员函数与子类中的同名成员函数是重载关系,所以访问父类中的同名成员函数需要加作用域
14、多态
父类中的函数需要加virtual
关键字,加上virtual
后,虚函数变成了一个虚函数指针vfptr
,指向了一个虚函数表,虚函数表内记录了虚函数的地址
多态实现计算器
#include<iostream>
#include<string>
using namespace std;
class AbstractCalculator {
public:
int a;
int b;
virtual int calculate() {
return 0;
}
};
class AddCalculator : public AbstractCalculator {
int calculate() {
return a + b;
}
};
class SubCalculator : public AbstractCalculator {
int calculate() {
return a - b;
}
};
int main() {
AbstractCalculator *ptr = new AddCalculator;
ptr->a = 10;
ptr->b = 10;
cout << ptr->a << " + " << ptr->b << " = " << ptr->calculate() << endl;
delete ptr;
ptr = new SubCalculator;
ptr->a = 11;
ptr->b = 10;
cout << ptr->a << " - " << ptr->b << " = " << ptr->calculate() << endl;
delete ptr;
return 0;
}
纯虚函数
// 纯虚函数,包含纯虚函数的类称为抽象类,无法实例化
// 若其子类没有重写该纯虚函数,否则也是抽象类,无法实例化
virtual int calculate() = 0;
父类的析构函数需要加virtual
关键字,虚析构函数;纯虚析构需要有声明,也需要有实现
15、文件操作
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
class Person {
public:
char name[64];
int age;
};
int main() {
/*
ofstream out;
out.open("./a.txt", ios::out | ios::app);
out << "Hello C++!" << endl;
out << "Hello Java!" << endl;
out << "Hello Python!" << endl;
out << "Hello 郑育强!" << endl;
out.close();
*/
/*
ifstream in;
string str;
char buffer[1024] = {0};
in.open("./a.txt", ios::in);
if (!in.is_open()) {
cout << "Fail to open file!" << endl;
} else {
cout << "==============1============" << endl;
// 遇到空格/换行停止读取
while (in >> buffer) {
cout << buffer << endl;
}
cout << "==============2============" << endl;
// 逐行读取
// 当读指针达到文件尾部时,seekg失效,需要先调用clear清除再代用seekg
in.clear();
in.seekg(0, ios::beg);
while (in.getline(buffer, sizeof(buffer))) {
cout << "方式2" << endl;
cout << buffer << endl;
}
cout << "==============3============" << endl;
// 逐行读取
in.clear();
in.seekg(0, ios::beg);
while (getline(in, str)) {
cout << str << endl;
}
cout << "==============4============" << endl;
// 逐个字符读取
in.clear();
in.seekg(0, ios::beg);
char c;
while ((c = in.get()) != EOF) {
cout << c;
}
}
in.close();
*/
// 二进制文件
ofstream ofs("person.txt", ios::out | ios::binary);
Person p = { "张三" , 18 };
ofs.write((const char *)&p, sizeof(Person));
ofs.close();
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open()) {
cout << "Fail to open file!" << endl;
} else {
Person ptr;
ifs.read((char *)&ptr, sizeof(Person));
cout << "name = " << ptr.name << " age = " << ptr.age << endl;
}
ifs.close();
return 0;
}
三、提高编程
1、函数模板
1、模板函基本格式
// 模板函数add
template<typename T>
T add(T a, T b) {
return a + b;
}
// 普通函数add
int add(int a, int b) {
return a + b;
}
template<typename T>
void swapElem(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}
template<typename T>
void swapData(T *a, T *b) {
T *temp = *a;
*a = *b;
*b = *temp;
}
2、函数模板的自动类型推到不会发生强制类型转换,而指定类型会发生强制类型转换
3、当模板函数的参数为引用或指针类型时,不会发生强制类型转换
4、当模板函数和普通函数同时满足函数调用时,优先调用普通函数,若想强制调用模板函数,空参数模板会强制调用模板函数
add(a, b); // 调用的是普通函数
add<>(a, b); // 空参数模板强制调用的是函数模板
5、函数模板对于自定义类型可能并不兼容,可使用模板具体化版本来解决自定义类型问题
template<class T>
bool comparator(T a, T b) {
if (a == b) {
return true;
}
return false;
}
// 利用模板函数具体化解决自定义类型问题
template<> bool comparator(Person p1, Person p2) {
if (p1.name == p2.name && p1.age == p2.age) {
return true;
}
return false;
}
2、类模板
1、类模板基本格式
// 全局函数的类外实现
// 1.先声明class
template<class NameType, class AgeType = int>
class Person;
// 2.写函数实现
friend void printPerson(Person<NameType, AgeType> p) {
cout << "name :" << p.name << " age:" << p.age << endl;
}
// 类模板可以有默认类型
// 类模板中的成员函数在调用时才会创建
template<class NameType, class AgeType = int>
class Person {
// 全局函数的类内实现
friend void printPerson<>(Person<NameType, AgeType> p) {
cout << "name :" << p.name << " age:" << p.age << endl;
}
// 全局函数类外实现,注意需要加空参数列表
//(这是一个普通函数,而不是函数模板,需要加空参数列表)
friend void printPerson2<>(Person<NameType, AgeType> p);
public:
NameType name;
AgeType age;
Person(NameType name, AgeType age) : name(name), age(age) {}
void showInfo() {
cout << "name :" << name << " age:" << age << endl;
}
// 类外实现
void setName(NameType name);
};
// 类模板成员函数类外实现
template<class NameType, class AgeType>
void Person<NameType, AgeType>::setName(NameType name) {
this->name = name;
}
2、模板类作为父类的两种实现方式
// 子类在继承类模板时,需要指定父类中的泛型
class Boss : public Person<string, int> {
};
// 子类想灵活指定父类的类型,也需要进行模板化
template<class T1, class T2, class T3>
class Employee : public Person<T1, T2> {
T3 t;
};
3、模板类分文件的两种实现方式
(1)使用时引用.cpp
文件而不是.h
文件,类模板的成员函数一开始并不会创建,只包含.h
文件会链接失败,直接包含cpp
,则编译器可以正确链接
(2)将.cpp
和.h
写到一个文件,文件后缀名改为.hpp
3、STL概述
1、STL六大组件:容器、算法、迭代器、仿函数、适配器、空间配置器
2、两种常用的遍历方式
3、位置参数都是迭代器
// 遍历方式1
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
cout << endl;
// 遍历方式2 需要包含#include<algorithm>
for_each(v.begin(), v.end(), func);
cout << endl;
4、string
1、string本质上是一个类,类里面维护着一个char *
变量
2、构造函数
3、string的赋值
4、string的拼接
5、string的查找与替换
6、字符串的比较
7、字符串的存取
8、字符串的插入与删除
9、字符串的截取
5、vector
1、基本概念
vector本质上是一个类,类中维护一个单端数组,进行扩展时,是申请更大的空间,并将旧数据拷贝到新数组中,其迭代器是支持随机访问的(数组连续存储的特性)。
2、构造函数
3、vector的赋值
4、vector对容量的操作
5、vector的插入与删除
6、vector中的数据存取
7、两个容器元素的互换
用swap
可收缩空间
8、预留空间
减少vector扩容的次数,vector的每次动态扩展都需要申请一片更大的空间并复制旧数据。
void vector_test() {
vector<int> vector1;
// 默认分配器 0 -> 1 -> 2 -> 4 -> 8 -> 16 -> 32
for (int i = 0; i < SIZE; i ++) {
//cout << i << " start => vector.data() => " << vector1.data() << ", vector.capacity() => " << vector1.capacity() << endl;
vector1.push_back(rand());
//cout << i << " end => vector.data() => " << vector1.data() << ", vector.capacity() => " << vector1.capacity() << endl;
}
cout << "1 vector1 => ";
for (auto elem : vector1) {
cout << elem << " ";
}
cout << endl;
// Iterator 无法直接打印
// 无法直接打印获取地址 cout << it << " ";
// 当vector大小进行扩展时,重新申请一块内存空间,因此元素的地址会发生改变
cout << "2 vector1(iterator) => ";
for (auto it = vector1.begin(); it != vector1.end(); it ++) {
cout << *it << " ";
}
cout << endl;
// Element access
cout << "3 vector.front() => " << vector1.front() << endl;
cout << "4 vector.back() => " << vector1.back() << endl;
cout << "5 vector.at(2) => " << vector1.at(2) << endl;
cout << "6 vector[2] => " << vector1[2] << endl;
// 发生扩容时,一般情况下,vector.data()的值会发生变化
cout << "7 vector.data() => " << vector1.data() << endl;
// size
cout << "8 vector.empty() => " << vector1.empty() << endl;
cout << "9 vector.size() => " << vector1.size() << endl;
cout << "10 vector.capacity() => " << vector1.capacity() << endl;
// potential size the container can reach
cout << "11 vector.max_size() => " << vector1.max_size() << endl;
// resize(n, val) 重置标识,若n > capacity,则会进行容器的扩容
// 若n<size, 则只保留前n个元素;若n > size,进行val存在则使用val填充剩余位置
vector1.resize(10);
cout << "12 after resize => vector.size() => " << vector1.size() << endl;
cout << "13 after resize => vector.capacity() => " << vector1.capacity() << endl;
// reserve(n) 将capacity扩大到n,不改变size
// n <= capacity时不进行任何操作
vector1.reserve(40);
cout << "14 after reserve => vector.size() => " << vector1.size() << endl;
cout << "15 after reserve => vector.capacity() => " << vector1.capacity() << endl;
// shrink_to_fit() 将capacity变为size;
vector1.shrink_to_fit();
cout << "16 after shrink_to_fit => vector.size() => " << vector1.size() << endl;
cout << "17 after shrink_to_fit => vector.capacity() => " << vector1.capacity() << endl;
// push_back
vector1.push_back(1088);
vector1.pop_back();
// pos_iterator每次都需要重新获取
// insert(pos_iterator, val)
// insert(pos_iterator, n, val)
// insert(pos_iterator, begin_iterator, end_iterator);
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
cout << "18 vector1 => ";
for (auto elem : vector1) {
cout << elem << " ";
}
cout << endl;
vector1.insert(vector1.begin(), 1088);
vector1.insert(vector1.begin(), 2, 88);
vector1.insert(vector1.begin(), &arr[0], &arr[10]);
cout << "19 vector1 => ";
for (auto elem : vector1) {
cout << elem << " ";
}
cout << endl;
// erase(pos_iterator)
// erase(begin_iterator, end_iterator);
// 删除第三个元素
vector1.erase(vector1.begin() + 2);
cout << "20 erase1 => vector1 => ";
for (auto elem : vector1) {
cout << elem << " ";
}
cout << endl;
// 删除第四和第五个元素
vector1.erase(vector1.begin() + 3, vector1.begin() + 5);
cout << "21 erase2 => vector1 => ";
for (auto elem : vector1) {
cout << elem << " ";
}
cout << endl;
vector<int> vector2 = {1,2,3};
// 将vector中的元素为10个8
vector2.assign(10, 8);
cout << "22 vector2 => ";
for (auto elem : vector2) {
cout << elem << " ";
}
cout << endl;
// 将数组[2, 9)区间内的元素赋值给vector2
vector2.assign(&arr[2], &arr[9]);
cout << "23 vector2 => ";
for (auto elem : vector2) {
cout << elem << " ";
}
cout << endl;
// 将vector1中的除前两个元素外,全部复制到vector2中
vector2.assign(vector1.begin() + 2, vector1.end());
cout << "24 vector2 => ";
for (auto elem : vector2) {
cout << elem << " ";
}
cout << endl;
// 交换两个vector中的元素
vector1.swap(vector2);
cout << "25 after swap vector1 => ";
for (auto elem : vector1) {
cout << elem << " ";
}
cout << endl;
cout << "26 after swap vector2 => ";
for (auto elem : vector2) {
cout << elem << " ";
}
cout << endl;
// 清空vector
cout << "27 before clear => vector.size() => " << vector1.size() << endl;
cout << "28 before clear => vector.capacity() => " << vector1.capacity() << endl;
vector1.clear();
cout << "29 after clear vector1 => ";
for (auto elem : vector1) {
cout << elem << " ";
}
cout << endl;
cout << "30 after clear => vector.size() => " << vector1.size() << endl;
cout << "31 after clear => vector.capacity() => " << vector1.capacity() << endl;
}
6、deque
1、基本概念
deque
本质上是一个类,类中维护一个双端数组,相对而言,对头部的插入删除操作的速度比vector
快,但vector
访问元素时速度比deque
快,其迭代器是支持随机访问的(数组连续存储的特性,虽然分为多个数组存储)。
只读迭代器const_iterator
2、构造函数
3、deque的赋值操作
4、插入与删除
5、数据存取
6、排序
7、Stack
1、基本概念
栈中只有栈顶元素才允许被访问,不可以遍历栈中元素
pop() 函数没有返回值,它的作用是移除堆栈顶的元素。因此,如果你需要获取被移除的元素,你应该先调用 top() 函数来获取堆栈顶元素的值,然后再调用 pop()
2、基本操作
8、Queue
1、基本概念
只可访问队头与队尾,不支持遍历行为
2、基本操作
8.1 priority_queue
// 自定义比较器
class mycomparison {
public:
bool operator()(const int& a, const pair<int, int>& b) {
// 从小到大排列
return a > b;
}
};
// 定义一个小顶堆
priority_queue<int, vector<int>, mycomparison> pri_que;
// 操作与queue相同
9、list
1、基本概念
链式存储,本质上是一个双向循环链表,其迭代器是双向迭代器,不支持随机访问
2、构造函数
3、赋值与交换
4、大小操作
5、插入与删除
6、数据存取
7、反转与排序
class Person {
public:
int age;
int height;
Person(int age, int height):age(age),height(height) {}
};
// 自定义类型Person比较器,先按年龄升序,再按身高降序
bool PersonCompartor(Person& p1, Person& p2) {
if (p1.age == p2.age) {
return p1.height > p2.height;
} else {
return p1.age < p2.age;
}
}
int main() {
list<Person> L;
Person p1(10, 150);
Person p2(40, 170);
Person p3(10, 140);
Person p4(30, 180);
Person p5(10, 160);
Person p6(10, 135);
L.push_back(p1);
L.push_back(p2);
L.push_back(p3);
L.push_back(p4);
L.push_back(p5);
L.push_back(p6);
// 自定义类型无法直接排序,需要传入比较器
L.sort(PersonCompartor);
for (list<Person>::iterator it = L.begin(); it != L.end(); it++) {
cout << "age: " << it->age << " height: " << it->height << endl;
}
return 0;
}
10、set
1、基本概念
所有元素在插入时会被自动排序,set/multiset
属于关联式容器,底层用二叉树实现,set
不可以有重复的元素,在插入时会自动去重,而multiset
可以有重复的元素
2、构造与赋值
3、大小与交换
不支持resize操作
4、插入与删除
- 更新
value
无法通过重新insert
实现,当key
已存在时,insert
是无效的,可以通过修改it->second
的值来达到目的 - 插入的数据类型为
pair<key_type, value_type>
- 迭代器的类型为
map<key_type, value_type>::iterator it
5、查找与统计
对于set
而言,count()
只可能为0或1
6、set
与multiset
的区别
set
不可以有重复数据,而multiset
可以;
set
插入数据的同时返回插入结果,表示插入是否成功,而multiset
不会做检测
7、对组pair
8、改变set内置类型的排序规则(仿函数)
11、map
1、基本概念
map
中所有元素都是pair
,pair
中第一个值为key(键值),第二个为value(实值),所有元素会根据键值自动排序,也是关联式容器,底层采用二叉树实现,multimap
允许存在键值重复的元素
2、构造与赋值
3、大小与交换
4、插入与删除
5、查找与统计
12、array
array
特点同普通数组类似,C++11实现了对普通数组的封装。
#include<iostream>
#include<array>
#define SIZE 20
using namespace std;
void array_test() {
array<long, SIZE> array1;
for (int i = 0; i < SIZE; i ++) {
array1[i] = rand();
}
// 设定array的大小为20,只对前5个元素进行赋值,后15个元素并非都是0
// 41 18467 6334 26500 19169 0 8 0 0 0 268501009 0 13764456 0 -1871664506 32766 8 0 0 0
for (int i = 0; i < SIZE; i ++) {
cout << array1[i] << " ";
}
cout << endl;
// 也就是判断array1.size()是否为0,返回0或1
cout << "Whether the array is empty => array1.empty() => " << array1.empty() << endl;
// 在array中,array.size()与array.max_size()总是是相等的
cout << "The size of array => array.size() => " << array1.size() << endl;
cout << "The max size of array => array.max_size() => " << array1.max_size() << endl;
// front()返回值,begin()返回地址
cout << "The first element of array => array.front() => " << array1.front() << endl;
cout << "The address of first element in array => &array.front() => " << &array1.front() << endl;
cout << "The first element reference of array => array.begin() => " << array1.begin() << endl;
cout << "The address of first element in array => array.data() =>" << array1.data() << endl;
// back()返回最后一个元素的值,end()最后一个元素下一个地址
cout << "The last element of array => array.back() => " << array1.back() << endl;
cout << "The address of last element in array => &array.back() => " << &array1.back() << endl;
cout << "The next address of last element in array => array.end() => " << array1.end() << endl;
// 获取array指定位置元素的两种方法
cout << "The second element of array => array.at(1) => " << array1.at(1) << endl;
cout << "array[1] => " << array1[1] << endl;
// 将数组中所有元素置为1
array1.fill(1);
// 将元素类型相同,大小相同的两个数组中的元素进行交换
array<long, SIZE> array2;
array2.fill(2);
array1.swap(array2);
cout << "array1 => ";
for (int i = 0; i < SIZE; i ++) {
cout << array1[i] << " ";
}
cout << endl;
cout << "array2 => ";
for (int i = 0; i < SIZE; i ++) {
cout << array2[i] << " ";
}
cout << endl;
// 深拷贝,赋值操作后,array1的修改操作不会影响array2
array1 = array2;
array1[0] = 3;
cout << "array1 => ";
for (int i = 0; i < SIZE; i ++) {
cout << array1[i] << " ";
}
cout << endl;
cout << "array2 => ";
for (int i = 0; i < SIZE; i ++) {
cout << array2[i] << " ";
}
cout << endl;
}
13、函数对象
重载了函数调用操作符()的类,其对象称为函数对象,仿函数是一个类,而不是一个函数
14、算法
16、转换函数
class Fraction {
public:
// 辗转相除法求最大公约数
static int getMaxCommonDivisor(int n, int m) {
int a = n % m;
while(a != 0) {
n = m;
m = a;
a = n % m;
}
return m;
}
// 加上explicit不会将该构造函数作为转换器
explicit Fraction(const int& numerator, const int& denominator = 1):numerator(numerator), denominator(denominator){
int p = Fraction::getMaxCommonDivisor(numerator, denominator);
if (p > 1) {
this->numerator /= p;
this->denominator /= p;
}
}
// 重载+运算符
Fraction operator+(const Fraction& f) const {
int n = numerator * f.denominator + f.numerator * denominator;
int d = denominator * f.denominator;
int p = Fraction::getMaxCommonDivisor(n, d);
return Fraction(n/p, d/p);
}
// 转换函数:Fraction=>double
operator double() const {
return (double)numerator / denominator;
}
// 打印函数
void print() const { std::cout << numerator << "/" << denominator << std::endl; }
private:
int numerator;
int denominator;
};
int main() {
Fraction f1(12, 8);
f1.print(); // 3/2
Fraction f2(5, 3);
f2.print(); // 5/3
Fraction f3 = f1 + f2;
f3.print(); // 19/6
Fraction f4 = f1 + 4;
f4.print(); // 11/2
Fraction f5(4, 10);
f5.print(); // 2/5
// 调用了Fraction转换函数operator double() const,将f5转换成了0.4
double d = 0.5 + f5;
// d = 0.9
cout << "d = " << d << endl;
return 0;
}
四、进阶补全
4.1 命名空间
- 1、命名空间不能定义到函数内部(必须全局定义)
- 2、命名空间可以嵌套定义
- 3、将命名空间的声明和定义分开
- 4、命名空间别名 namespace newName = veryLongName;
4.2 引用
语法:Type& refName = var;
- &在此不是求地址符号,而是标识作用
- 必须在声明时初始化,不能有NULL的引用(编写代码时就会报错)
- 初始化后无法改变(相当于对引用进行重新赋值,而不是改变引用的变量)
int * const refName = &var <=> int& refName = var
引用的本质是一个常指针,引用所占空间与指针大小相同- 为什么说引用不占用空间? 有些编译器会将引用直接替换成原来的变量
- 引用的对象是普通数据类型时,与指针差不多,引用在函数形参为对象的引用及函数的返回值为对象的引用上使用非常广泛
// 推荐用引用
void func1(Person &p) {
p.age = 12;
}
void func1(Person *p) {
p->age = 12;
}
4.3 内联函数
- C语言中,将简单的操作写成宏,而不是定义成函数,可以减少函数栈的调用但是宏定义是粗暴的替换,且不会进行参数类型检查
- 而内联函数同样不会进行函数栈的调用,且有类型检查
内联函数的声明和定义必须在一起,作用域只在定义的文件内
将函数定义为内联函数,只是给编译器建议,编译器并不一定会将其作为内联函数处理,编译器会自己分析
4.4 函数重载
g++在编译函数时,函数符号会带上参数类型(命名空间+函数名+参数类型 )
4.5 类和对象
- 类不会占用程序的内存空间,对象才会
C语言的结构体可以通过函数指针类定义行为
- 自定义了任意构造函数后,默认的无参构造函数不会自动生成,一般都需要自己定义无参构造函数
- 初始化成员列表
Box(int x, int y): width(x), height(y){}
,必须使用初始化成员列表的三种情况:- 存在引用类型成员变量
- 存在const修饰的成员变量
- 存在类成员变量,并且类成员没有实现无参构造函数
new
与delete
// 堆上申请一个int类型大小的空间(4字节),并初始化为10 int *p = new int(10); // 堆上申请4个int类型大小的空间(4*4 字节),未初始化 int *q = new int[4]; delete p; delete[] q;
- new/delete和malloc/free
- new/delete为C++的操作运算符,malloc/free为C的标准库函数,前者效率高
- new能自定计算需要分配的空间
- new直接带具体的指针类型,而malloc时void类型指针
- new时类型安全的,
int *p = new float[2];
会报错 new/delete会调用构造和析构函数,而malloc和free不会
- 先构造成员变量,再构造本身;析构顺序相反
- 当使用初始化后的对象去初始化另一个对象时,会调用拷贝构造函数
Test(const Test& t)
,而不是无参构造函数 - 当成员变量中存在指针类型时
- 需要重写析构函数,自己释放堆空间
- 使用对象初始化另一个对象时,两个对象中的指针指向同一个堆空间,需要重写拷贝构造函数(深拷贝)
- 函数的参数为class时,若直接使用class,则再调用函数时,会通过拷贝构造函数生成一个临时对象,应该使用引用或者指针
- 静态成员变量必须定义并初始化
int Test::global_sum = 10;
- 面向对象内存模型
- sizeof(className)只包含成员变量的大小,成员函数位于代码段
- 同一个类的所有对象共用一份位于代码段的函数,成员函数存在一个隐藏的指针变量,指向当前操作的对象
- 静态成员变量位于数据段上
虚函数指针位于数据段上
-------- | 栈 | -------- | 堆 | -------- | 数据段| -------- | 代码段| --------
4.6 string类
- to_string() 将数字型变量转换成字符串
- 常用函数:find(), append(), insert(), replace(), erase(), compare()
- find函数如果找不到,返回string::npos
4.7 继承
- public继承保留父类的访问控制级别,protected继承父类中的public访问级别变为protected,private继承父类中的所有访问级别变为private
- 父类的private成员在子类中依然存在,但是无法访问
- 子类默认调用的时父类的无参构造函数,要调用父类的有参构造函数,需要使用初始化成员列表
- 通过子类的对象访问父类中同名的成员变量
obj.A::a_x
,避免子类出现与父类同名的成员变量 - 子类存在与父类同名的成员函数,会隐藏父类的同名成员函数(尽管参数不同),需要通过父类名作用域进行访问
- 菱形继承/环形继承 通过virtual关键字解决,在继承公共基类时,添加virtual关键字,虚继承
4.8 多态
- 函数重载、运算符重载、泛型编程
- 默认生成的函数:无参构造,析构函数,拷贝构造函数,赋值运算符重载函数(默认实现的是浅拷贝)
- 后置++需要构建临时对象,因此前置++的效率比较高
- 不能返回栈上对象的引用(函数内部定义的对象)
- 友元函数friend
- 友元函数是普通函数(也可以是另一个类的成员函数),不是当前类的成员函数,没有this指针
- 友元函数在类中声明不受访问控制标志的影响
- 可以同时是多个类的友元函数
- 输出运算符的重载需要使用friend,输出运算符为双目运算符,但是第一个操作数是ostream而不是要进行操作的类
- 当类A中的大部分函数需要访问类B的私有属性时,将类A声明为类B的友元类
- 友元关系不能继承
- 隐藏与重写
在子类中存在与父类函数名和形参相同的成员函数,若父类的成员函数有virtual修饰,则是重写,通过将子类赋值给父类的指针,通过父类指针调用的是子类的同名成员函数;若没用virtual修饰,则是隐藏,相同情况下调用的是父类的成员函数 - 虚函数如何实现动态多态
- 动态多态的的指向效率低于静态多态,只有必要的时候才使用虚函数
- 虚函数指针,本质上是一个指向函数的指针,每一个虚函数对应一个虚函数指针,在编译时就产生了虚函数指针,存在于数据段上,类的所有对象共用虚函数指针
构造函数不能时虚函数,而析构函数时可以是虚函数;
基类中的析构函数不是虚函数时,通过基类指针指向派生类时,释放内存不会调用派生类的析构函数,一般需要把基类的析构函数定义成虚函数,防止内存泄露
。- 如果有基类指针指向派生类对象,编译器是如何处理的?编译器先检查被调用的函数是否为虚函数,如果不是虚函数,采用静态编译,调用的是基类的函数,不论派生类是否存在同名的函数;如果是虚函数,会访问派生类对象的虚表指针(运行时才能确定调用的函数),在虚函数表中查找调用函数的虚函数指针。
- 每一个存在虚函数的实例化对象都持有一个虚表指针,而且是对象内存中的首地址
- 存在虚函数的每一个类都对应一个虚函数表
4.9 C++对C的扩展
const
-
const修饰的对象是只读的
-
const修饰普通变量
-
const修饰成员变量,只能在初始化成员列表中赋值
-
const修饰成员函数
int get_x() const {return x;}
,常成员函数,不能修改成员变量的值 -
const不能修饰全局函数
-
const修饰对象
const Animal a;
,常对象,对象的所有成员变量都不能修改,只能调用const修饰的成员函数 -
const修饰引用,被引用的变量不能修改
-
const修饰函数返回值,返回值是普通变量或者对象用const修饰没有意义(不能通过左值修改右值);返回值是指针或者引用才有意义。
extern “C”
#ifdef __cplusplus
配合extern "C"
实现C代码可用于g++和gccgcc example.c -fPIC -shared -o example.dll
g++ main.cpp -L. -lexample -o main
-L.告诉编译器需要链接的库文件在当前目录下
NULL与nullptr
- 在C语言中,NULL == (void *)0; 在C++中,NULL == 0,nullptr == (void *)0
- 当C++中存在函数重载,形参分别为
int *p
与int p
,当实参为NULL
时,由于NULL == 0,编译器会不知道调用哪个函数
4.10 异常
class FrameError : public exception {
public:
// 格式固定
const char *what() const throw(){
return "Frame header error!";
}
};
int main() {
vector<int> arr(3, 10);
try {
// arr.at(3) = 100;
// throw "error hahaha!";
// throw 1;
FrameError err;
throw err;
} catch(const char *msg) {
// 字符串异常
cout << "const char *msg, msg = " << msg << endl;
} catch(int code) {
// 整型异常
cout << "const int code, code = " << code << endl;
} catch(FrameError &err) {
cout << err.what() << endl;
} catch(exception &e) {
cout << e.what() << endl;
} catch(...) {
// 任意异常
cout << "..." << endl;
}
return 0;
}
4.11 智能指针
11.1 动态内存的申请与释放
- new基本类型最好用()初始化,
int *p = new int
,值是未定义,int *p = new int()
,值是0 - MFC应用程序在程序退出时能够检查内存泄漏
- new做了两件事件,调用函数
operator new()
分配内存,再调用构造函数;delete先调用析构函数,再调用函数operator delete()
释放内存 - 空类占一个字节,占位置
B *b = new B[2];
空类数组占用了2 + 4字节,额外的4字节来记录需要析构的次数(内置类型没有这部分额外的4字节,没有自定义析构函数也没有这额外的4个字节)
11.2 智能指针
- 裸指针,直接用new返回的指针
- auto_ptr(C++98), unique_ptr(C++11), shared_ptr(C++11), weak_ptr(C++)[本质都是类模板]
- auto_ptr(C++98)已经被unique_ptr(C++11)取代,已弃用
11.3 shared_ptr
- shared_ptr的大小为裸指针的两倍,一个为裸指针,一个为指向控制块的指针
- 工作原理:引用计数
- make_shared 标准库中的函数模板
- 不能用裸指针初始化多个shared_ptr,不建议使用裸指针
- 优先使用
shared_ptr<int> ptr2 = make_shared<int>(100);
,而不是shared_ptr<int> ptr1(new int(100));
,后者可能申请两次内存 - get()返回的裸指针不能delete(有些第三方库只支持裸指针)
- 成员函数需要返回this指针的智能指针,需要使用
enable_shared_from_this
class C : public enable_shared_from_this<C> { public: // 返回对象的this指针 // 通过weak_ptr()的lock()函数来实现 shared_ptr<C> getSelf() { return shared_from_this(); } };
- 智能指针的控制块随着第一个shared_ptr的创建而创建
11.4 移动语义move
```cpp
shared_ptr<int> ptr1 = make_shared<int>(100);
// 移动构造一个新的智能指针对象
// ptr1变为空
shared_ptr<int> ptr4(move(ptr1));
shared_ptr<int> ptr5 = move(ptr4);
```
11.5 unique_ptr
- 没有控制块,占用空间与裸指针相同(自定义删除器则可能增加占用空间)
- 同一时刻只能有一个unique_ptr指向一块内存
- 不支持拷贝,不支持赋值
- release()返回裸指针,智能指针被置为空,需要自己接管裸指针,负责内存的delete
- reset()不带参数,释放智能指针,以及其指向的内存
- unique_ptr = nullptr, 释放智能指针,以及其指向的内存
- reset()带参数(裸指针),智能指针指向传入的裸指针的内存
- swap()交换两个智能指针指向的对象
- unique_ptr move给shared_ptr,其对象由shared_ptr接管,unique_ptr被释放
11.6 定义函数指针的三种方式
// 定义函数指针的三种方式,指向返回值为[void], 参数为[string *]的函数
typedef void(*fp1)(string *);
using fp2 = void(*)(string *);
typedef decltype(fun) *fp3;
11.7 智能指针总结
- 设计思想: 代理释放内存,防止内存泄漏
- 弃用独占式智能指针auto_ptr, 不能在容器中保存,也不能从函数中返回,用auto_ptr进行赋值,右值会变为空
- 首选unique_ptr,需要多个指向同一对象才使用shared_ptr
5. 查漏补缺
5.1 结构体与类的字节对齐&strlen()&sizeof()
- 结构体的字节对齐方式以结构体中占用空间最大的数据类型为准,【C/C++】结构体对齐详解
- strlen()和sizeof()的测试结果如下,cout行后的注释即为输出
#include <iostream>
#include <cstring>
using namespace std;
// 包含1字节的char,4字节的int,按4字节对齐,1+4+4 = 9 => 12
struct {
char a;
int b;
int c;
} ap;
class cp{
char a;
int b;
int c;
};
// 包含1字节的char,按1字节对齐,1+2 = 2 => 2
struct {
char a;
char b;
} ap2;
class cp2{
char a;
char b;
};
// 包含1字节的char,4字节的int,8字节的double,按8字节对齐,1+4+8 = 13 => 16
struct {
char a;
int b;
double d;
} ap3;
class cp3{
char a;
int b;
double d;
};
int main() {
char *s1 = "hello1";
char s2[] = "hello1";
// 触发4字节对齐
cout << sizeof(ap) << " " << sizeof(cp) << endl; // 12 12
// 没有触发4字节对齐
cout << sizeof(ap2) << " " << sizeof(cp2) << endl; // 2 2
// 触发8字节对齐
cout << sizeof(ap3) << " " << sizeof(cp3) << endl; // 16 16
// 对于strlen(), char *和char []都是获取到实际的字符串长度
// 对于sizeof(),
// char *获取到的是一个指针变量占用的空间4字节
// char []获取到实际的字符串长度 + 1(包括结束字符'\0')
cout << strlen(s1) << " " << sizeof(s1) << endl; // 6 4
cout << strlen(s2) << " " << sizeof(s2) << endl; // 6 7
return 0;
}
5.2 C和C++中static的基本作用
- 静态局部变量:在函数内部声明的静态变量,在第一次执行到该变量定义时被初始化,并在函数调用结束后不会被销毁,而是保持其值直到下次调用。
- 静态全局变量:在全局作用域声明的静态变量只在其声明的文件内可见,从而避免了全局命名空间的污染。
- 静态函数:在函数前加static关键字,可以使函数只在定义该函数的文件内可见。
- 静态成员变量和函数:在类中声明的静态成员变量和函数属于类本身,而不是任何特定的对象。
【C++ 】详解Static关键字
5.3 C++编译过程
- 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
- 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
- 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
- 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件
C++ 编译与底层
5.4 枚举与强枚举
- 无论是枚举还是强类型枚举,默认是int型(4字节),第一个为0
- 多个枚举类型不能存在相同的枚举名称,强类型枚举可以同名,但是强类型枚举访问都需要添加枚举类作用域
- 枚举类型必须是整数,不能定义为float和double
- 枚举类型不能进行算术运算
- 枚举值可以指定为负值,并且可以为多个枚举赋相同的值
- 枚举类型默认为int类型,占4个字节(枚举类型所占空间与其指定的类型相同)
#include <iostream>
using namespace std;
enum Color{
RED,
BLUE,
YELLOW = -1,
ORANGE = -1,
BLACK,
WHITE
};
// 64位系统下测试long类型,也占4字节
// 指定为long long类型,大小为8字节
enum LongColor : long long{
RED1,
BLUE1,
YELLOW1
};
enum class ColorClass {
RED,
BLUE,
YELLOW
};
class T {
ColorClass cc1;
ColorClass cc2;
Color c;
};
int main() {
// 编译错误,不能直接赋值,需要强转
// Color c = 1;
Color c = (Color)1;
cout << c << endl; // 1
cout << "YELLOW = " << YELLOW << endl; // YELLOW = -1
cout << "ORANGE = " << ORANGE << endl; // ORANGE = -1
cout << "BLACK = " << BLACK << endl; // BLACK = 0
cout << "WHITE = " << WHITE << endl; // WHITE = 1
// 编译错误,不能进行算术运算
// c = c + 1;
cout << "sizeof(LongColor): " << sizeof(LongColor) << endl; // sizeof(LongColor): 8
cout << "sizeof(ColorClass): " << sizeof(ColorClass) << endl; // sizeof(ColorClass): 4
cout << "sizeof(T): " << sizeof(T) << endl; // sizeof(T): 12
return 0;
}
5.5 联合体union
- 联合体的大小以最大的数据类型为准, 并且必须是其他数据类型大小的整数倍
- 当联合体中存在多个整数类型,如下的s和a,当联合体的值不超过short类型的范围时,s和a都可以输出正确的值
#include <iostream>
using namespace std;
// 1. 联合体的大小以最大的数据类型为准, 并且必须是其他数据类型大小的整数倍
// 2. 当联合体中存在多个整数类型,如下的s和a,当联合体的值不超过short类型的范围时,s和a都可以输出正确的值
class T {
public:
int a;
int b;
char c;
};
// T占12字节,12不是8的整数倍,字节对齐,扩展到16字节
union U {
short s; // 2字节
int a; // 4字节
long b; // 4字节
long long c; // 8字节
T t; // 12字节
};
int main() {
U u;
u.s = 100;
u.a = 200;
cout << u.s << endl; // 200
cout << u.a << endl; // 200
u.a = 10000000; // 已经超出short的表示范围
cout << u.s << endl; // -27008
cout << u.a << endl; // 10000000
// cout << u.t.a << endl; // 10000000
cout << sizeof(T) << endl; // 12
cout << sizeof(U) << endl; // 16
return 0;
}
函数指针?
归并排序?
字典树?
C++11和C++17的新特性