C++学习手册,主要是在C的基础上写出一些C++不同于C的点,部分C语言相通部分就不再赘述。
环境搭建
使用 VS Code 搭建C++开发环境(MAC)
- 在VS Code上安装好相应插件:C/C++、CodeLLDB
- 新建工作目录,编写C++文件
- 在Debug中创建launch.json文件,选择LLDB
- 将
<your program>
替换为${fileBasenameNoExtension}
- 选择cpp文件,建立Build文件,Shift+Command+P切出面板,选择Tasks:Configure Task -> C/C++ clang++ build active file
- 在”args”中添加C++标准
"-std=c++2a"
- 在launch.json中的”configurations”中添加
"preLaunchTask": "C/C++: clang++ build active file"
(与tasks.json中的label一致)
Xcode引入iostream库失败
提示 ‘iostream’ file not found的解决办法:
在Build Settings -> Search paths ->System Header Search Paths中添加路径
1 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/ |
添加后,C++库引入成功。
从C到C++
输入输出
C++输入输出需使用iostream库
标准输出cout
std :: cout 是输出内容,<< 是输出运算符,std :: endl 结束改行,相当于一个”\n”
1 | std::cout << "Hello, World!" << std::endl; |
1 | std::cout << "Hello, World!\n" ; |
标准输入cin
std :: cin 是输入内容,>> 是输入运算符。
1 |
|
使用标准库中的名字
std::表示cout和cin是定义在名为std的命名空间中的,可以通过using namespace来进行命名空间的缩略。
1 | using namespace std; |
控制流
while语句
while语句反复执行一段代码,直至条件判断为错误。
读取数量不定的输入数据
利用std::cin进行输入不定的输入。
1 | /** |
已经定义了value为int型,所以在 cin>>value 输入不为int型时,则判断为false。
也可用文件结束符,输入Ctrl+D
for语句
for(初始化语句; 循环条件; 表达式)
遍历数组中的元素
一维数组
1
2
3
4
5
6
7
8
9
10
11
using namespace std;
int main()
{
int scores[10] = {0};
for (auto i : scores) { //遍历数组scores中的元素
cout << i << endl;
}
return 0;
}多维数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using namespace std;
int main()
{
int ia[2][5] = {0};
for (auto &row : ia) { //遍历二维数组ia外层数组的每一个元素(这里相当于遍历每行)
for (auto &col : row) { //遍历二维数组ia内层数组的每一个元素
cout << col;
}
cout << endl;
}
return 0;
}
函数重载
同一作用域内,可以有同名函数,给函数多个定义,但形参必须不同,加以区分。
形参不同,指参数的类型、参数的个数和参数的顺序,至少有一个不同。
1 |
|
C++基础
变量和基本类型
类
相当于把各种数据打包组成了一个集合来调用。
下面以建立和使用一个书籍销售单类Sales_data为例。
建立头文件
为保障各文件中的类的定义一致,类通常被定义在头文件中。这里我们建立一个名为Sales_data.h的头文件来转载Sales_data类的定义。
头文件一般最好进行预处理,添加头文件保护符:
#define | 把名字设定为预处理变量 |
---|---|
#ifdef | 当且仅当变量已定义时为真 |
#ifndef | 当且仅当变量未被定义时为真 |
#endif | 判断为真后,执行到#endif结束 |
1 |
|
使用类定义
调用该.h文件
1 |
定义类变量
1 | Sales_data data1; |
利用.来访问类中的对象
1 |
|
迭代器
迭代器是一种检查容器内元素并遍历元素的数据类型。可以替代下标访问vector对象的元素。可以理解为指向容器内元素的指针。
使用迭代器
begin 返回指向第一个元素的迭代器,end 返回指向容器尾元素的下一个位置的迭代器(实际上是不存在的)。当begin和end返回的为同一迭代器,则该容器为空。
不在意迭代器的类型,一般定义为auto。
迭代器运算符
功能 | |
---|---|
*iter | 返回迭代器iter所指的元素的引用 |
Iter->mem | 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 令iter指示容器中的下一个元素 |
—iter | 令iter指示容器中的上一个元素 |
iter1 == iter2 / iter1 != iter2 | 判断两迭代器是否相等。如两迭代器指示的是同一个元素或者它们是同一个容器或者它们是同一个容器的尾后迭代器,则相等。 |
1 | /** |
迭代器运算
代码 | 功能 |
---|---|
iter + n / iter - n | 加或减n个位置得到一个迭代器 |
iter += n / iter -= n | 加或减n个位置得到一个迭代器赋给iter本身 |
iter1 - iter2 | iter1与iter2之间的位置差 |
> 、>=、<、<= | 比较位置 |
try语句块和异常处理
throw表达式
throw表达式,异常检测部分使用throw表达式来表示它遇到了无法处理的问题。
1 | /** |
当输入不为数字时,运行到throw语句时卡住,并输出异常信息。
try语句块
try语句块,异常处理部分使用try语句块处理异常。try尝试一个代码块,如有异常通常会被catch捕捉处理。
1 | try{ |
C++面向对象
C++面向对象的三大特性:封装、继承、多态。
万事万物都是对象,具有相同属性的对象可以抽象成一个类。
类和对象
类和对象的定义
把类的所有成员(变量和函数)封装起来,并加以权限限制。
- 语法:
1
2
3
4
5class 类名 {
访问权限:
成员变量
成员函数
}; 示例:设计一个圆类,求圆的周长
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using namespace std;
const double PI = 3.14;
//定义一个圆类
class Circle
{
public: //访问权限
//成员变量
double r; //半径
//成员函数
//计算周长
double calculate_circumference () {
return 2 * PI * r;
}
};
int main () {
Circle c1; //声明一个圆对象
c1.r = 10;
cout << "周长为:" << c1.calculate_circumference() << endl;
return 0;
}成员函数也可在类的外部,使用范围解析运算符
::
定义1
2
3
4
5
6
7
8
9
10
11
12
13class Circle
{
public:
double r;
//成员函数声明
double calculate_circumference();
};
//成员函数定义
double Circle::calculate_circumference () {
return 2 * PI * r;
}
访问权限
类成员的访问限制共三种:public
、private
、protected
- 公共权限
public
- 类内、类外都可访问
- 保护权限
protected
- 类内可访问,派生类可访问,类外不可访问
- 私有权限
private
- 只允许类内访问
- 如果没有使用任何访问修饰符,默认为私有权限
访问权限 | public | protected | private |
---|---|---|---|
类内 | 可访问 | 可访问 | 可访问 |
派生类 | 可访问 | 可访问 | 不可访问 |
类外 | 可访问 | 不可访问 | 不可访问 |
struct
和class
的区别:struct
和class
都可以表示类,但默认的权限不同。
如果成员没有使用任何访问修饰符,struct
默认为公共成员,class
默认为私有成员。
类内访问 & 类外访问
1 |
|
一般都把成员变量设置为
private
权限,然后在public
中设置读写函数,以便控制读写权限。
构造函数/析构函数
构造函数/析构函数是两个特殊的成员函数,分别在创建对象/销毁对象时被自动调用,用于完成对象初始化/清理工作。
即使不主动编辑构造函数/析构函数,编译器也会自动提供两个空函数作为构造函数/析构函数。
- 构造函数
- 创建对象时,自动调用一次,用于完成对象初始化
- 函数名与类名相同
类名() {}
- 可以有参数,不会有返回值,也不会返回
void
析构函数
- 销毁对象时,自动调用一次,用于完成清理工作
- 函数名与类名相同,加个前缀
~
,~类名() {}
- 不能有参数,不会有返回值,也不会返回
void
语法框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class 类名
{
private:
/* data */
public:
类名(/* args */); //构造函数,可以有参数
~类名(); //析构函数
};
类名::类名(/* args */)
{
}
类名::~类名()
{
}示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using namespace std;
class Line
{
private:
int length;
public:
Line(); //构造函数
~Line(); //析构函数
};
Line::Line() {
cout << "调用构造函数" << endl;
}
Line::~Line() {
cout << "调用析构函数" << endl;
}
int main () {
Line line; //创建对象,自动调用构造函数
return 0; //销毁对象时,自动调用析构函数
}
构造函数的分类和调用方式
无参构造
函数结构
1
2
3className() {
...
}调用方法(定义对象)
1
className c;
定义无参构造的对象的错误写法
className c();
,不能加()
,不然会与函数的声明弄混。
有参构造
以int
型参数为例
函数结构
1
2
3className(int x) {
...
}调用方法(定义对象)
1
className c(10); //方法一:括号法
1
className c = className(10); //方法二:显示法
1
className c = 10; //方法三:隐式转换法
示例
1 | Line::Line(int a, int b) { |
可以使用初始化列表来初始化字段,两种方法等效
1 | Line::Line(int a, int b) : A(a), B(b) |
拷贝构造
拷贝构造函数,即复制一个对象,生成新对象。
函数结构
1
2
3className(const className &obj) {
...
}const
防止被拷贝的数据obj被修改,且须用引用传递,而不能用值传递。调用方法(定义对象)
1
className c(obj); //方法一:括号法
1
className c = className(obj); //方法二:显示法
1
className c = obj; //方法三:隐式转换法
调用时机
使用一个对象,拷贝出一个新对象
1
2Line l1(10);
Line l2(l1); //调用拷贝构造值方式传递参数时,复制副本
1
2
3void func(Line l) { //值传递,复制原对象l,调用拷贝构造
...
}值方式返回局部对象时,复制副本
1
2
3
4void func( {
...
return l; //复制原对象l,返回副本,调用拷贝构造
}
无参/有参/拷贝构造示例
定义line
类,分别用无参/有参构造函数,定义两个length
相同的对象
1 |
|
构造函数的调用规则
默认情况下,编译器会自动给一个类添加3个函数:
- 默认构造函数(无参,空函数)
- 默认析构函数(空函数)
- 默认拷贝构造函数(对属性进行值拷贝)
如用户自行定义了有参构造函数,编译器将不再提供默认无参构造函数,但依旧提供默认拷贝构造函数;
如用户自行定义了拷贝构造函数,编译器将不再提供其他构造函数。
对象参数的引用传递
传递对象参数时,最好用引用传递
void func(className &obj);
如为值传递,即
void func(className obj);
,调用该函数时,要复制生成新的对象,调用拷贝构造函数,结束时,还需要调用析构函数来做清理工作。时间效率低。如为引用传递,即
void func(className &obj);
,调用该函数时,直接用原对象,不需要生成新对象。当不修改对象时,应当将参数声明为const引用。
拷贝构造函数的参数必须是引用,且最好用const引用
className(const className &obj);
如果使用值传递,即
className(className obj);
,则传值时会调用拷贝构造函数,会出现无穷递归调用拷贝构造函数的情况,所以拷贝构造函数的参数不能使用值传递。
深拷贝/浅拷贝
浅拷贝,即简单的赋值拷贝,默认拷贝构造即是浅拷贝。
如定义一个类
1 | class Person { |
编译器会自动生成默认拷贝构造函数,即
1 | Person(const Person &obj) { |
调用拷贝构造函数,即可进行简单拷贝,也就是浅拷贝。
1 | Person p1(10); |
浅拷贝可以处理一般的对象拷贝,所以不需要额外写拷贝构造函数,直接用默认的拷贝构造函数,即可进行浅拷贝。
但一旦类带有指针变量,用浅拷贝就会出现错误。
如下面的类中,带有指针变量
1 | class Person { |
如果使用编译器的默认拷贝构造函数,进行浅拷贝
1 | Person(const Person &obj) { |
1 | Person p1(10); |
p2
的height
与p1
的height
完全相同,都是指向堆区同一地址的指针。
在析构函数释放空间时,先释放完p1
的height
,对p2
析构时,height
已经释放,无法重新释放,发生错误。
所以类带有指针变量,并有动态内存分配,则必须自行定义拷贝构造函数,进行深度拷贝。
深拷贝,即在堆区重新申请空间,内容相同,但是不同的地址。
1 | class Person { |
this指针
每一个对象都能通过this
指针来访问自己的地址,this
指针指向被调用成员函数的所属对象。
主要用途:
- 形参和成员变量同名时,用
this
指针加以区分 - 返回对象本身
1 | class Person |
静态成员
在类成员前加上static
,称为静态成员。当声明成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本,静态成员在类的所有对象中是共享的。
静态成员变量
- 该类的所有对象共享同一份数据
- 编译阶段分配内存
- 类内声明,类外初始化
- 访问静态成员,可以用
p1.A
或者Person::A
1 |
|
静态成员函数
该类的所有对象共享同一个函数
静态成员函数只能访问静态成员
静态成员函数与普通成员函数的根本区别在于:
- 普通成员函数有
this
指针,可以访问类中的任意成员 - 静态成员函数没有
this
指针,只能访问静态成员(包括静态成员变量和静态成员函数)
- 普通成员函数有
静态成员函数的作用:方便调用
1 | class Solution { |
调用func1()
时,需要先生成类对象,才能调用
1 | Solution s; |
调用静态成员函数func2()
时,可以直接调用
1 | Solution::func2(); |
const常函数和常对象
如果不希望数据被修改,可以加上const
关键字来修饰成员变量、成员函数、对象。
常函数
- 成员函数后加
const
- 常函数内不能修改成员变量
- 如果成员变量前加
mutable
,则可在常函数内改变该变量
1 | class Person { |
成员函数中调用
m_A
,实际是调用了this -> m_A
成员函数的
this
指针,本质是指针常量,也就是指针本身是一个常量,地址不变,即Person * const this;
,此时this
一直指向对象本身如果为常函数,
const
修饰的是this
指针的指向,this
指针的指向的值内容不变,即const Person * const this;
,此时this
指向对象的内容不能被修改
get
类型的成员函数一般都采用常函数,只需获取,不需修改
常对象
- 对象前加
const
- 不能修改一般的成员变量,但可以修改
mutable
的成员变量 - 常对象只能调用
const
常函数(不能调用普通函数,因为普通的成员函数可能会修改成员变量)
1 |
|
友元
友元定义在类的外部,不属于类的成员,但有访问private
和protected
的权限,在类中用关键字friend
声明函数/类,即可将其设定为友元。
友元函数
将全局函数加上关键字friend
,在类中声明为友元,即可使其能够访问private
和protected
的权限成员。
尽管在类中有声明,但友元函数并不是成员函数。
1 |
|
友元类
将类加上关键字friend
,在类中声明为友元,即可使其能够访问private
和protected
的权限成员。
1 |
|
运算符重载
同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
函数重载和普通的函数重载一致,利用形参的不同加以区分。
运算符重载,则是重新定义运算符,以适应类的运算。
运算符重载是通过函数实现的,它本质上是函数重载。可以作为类的成员函数,还可以作为全局函数。
在运算符前加上关键词operator
,作为声明时的函数名。
- 可重载的运算符
类型 | 运算符 | ||
---|---|---|---|
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) | ||
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) | ||
逻辑运算符 | \ | \ | (逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) | ||
自增自减运算符 | ++(自增),—(自减) | ||
位运算符 | \ | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) | |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, | =, ^=, <<=, >>= | |
空间申请与释放 | new, delete, new[ ] , delete[] | ||
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
- 不可重载的运算符
.
:成员访问运算符.*
,->*
:成员指针访问运算符::
:域运算符sizeof
:长度运算符?:
:条件运算符#
: 预处理符号
负号-
重载(一元)
- 作为类的成员函数
1 | class Complex { |
- 还可以作为全局函数
1 | class Complex { |
调用-c
,即可将m_i
、m_j
取反
1 | Complex c(1,2); |
加号+
重载
1 | class Complex { |
调用a+b
,即可将m_i
、m_j
相加
1 | Complex a(1,2), b(2,3); |
关系运算符==
重载
1 | class Complex { |
继承
类与类之间可以有继承关系,已有一个基类,可以用一个派生类来继承基类
- 语法:
class 派生类 : 继承方式 基类
1 | //基类 |
Dog
就是Animal
的派生类,可以继承基类的成员。
派生类可以继承基类的所有成员,但只有public
和protected
成员能被访问到,private
成员可以继承,但无法访问。
继承方式
继承方式分为三种:公共继承public
、保护继承protected
、私有继承private
例如一个基类A
1 | class A { |
派生类B
按不同继承方式,成员会继承为不同的权限
公共继承
public
原
public
、protected
依旧以public
、protected
继承,不可访问private
。1
2
3
4
5
6
7
8class B : public A {
public:
int a;
protected:
int b;
不可访问:
int c;
};保护继承
protected
原
public
、protected
以protected
继承,不可访问private
。1
2
3
4
5
6
7class B : protected A {
protected:
int a;
int b;
不可访问:
int c;
};私有继承
private
原
public
、protected
以private
继承,不可访问private
。1
2
3
4
5
6
7class B : private A {
private:
int a;
int b;
不可访问:
int c;
};
多继承
一个派生类继承了多个基类。
1 | class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… |
例如,
1 | class Son : public Mother, public Father { |
构造和析构顺序
构造顺序、析构顺序镜像相反,先构造的后析构,先析构的后构造。
继承关系中,
构造顺序:基类构造 -> 派生类构造
析构顺序:派生类析构 -> 基类析构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using namespace std;
class Father {
public:
Father() {
cout << "Father构造" << endl;
}
~Father() {
cout << "Father析构" << endl;
}
};
class Son : public Father {
public:
Son() {
cout << "Son构造" << endl;
}
~Son() {
cout << "Son析构" << endl;
}
};
int main() {
Son s;
return 0;
}输出:
Father构造
Son构造
Son析构
Father析构当其他类作为本类成员时,
构造顺序:成员类构造 -> 本类构造
析构顺序:本类析构 -> 成员类析构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using namespace std;
class A {
public:
A() {
cout << "A构造" << endl;
}
~A() {
cout << "A析构" << endl;
}
};
class B {
public:
B() {
cout << "B构造" << endl;
}
~B() {
cout << "B析构" << endl;
}
private:
A a;
};
int main(void){
B b;
return 0;
}输出:
A构造
B构造
B析构
A析构
同名成员处理
如果派生类和基类中有相同成员重名,那么就会遮蔽从基类继承过来的成员,使用派生类成员。
如基类Father
和派生类Son
中都有成员变量m_A
1 | Son s; |
基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
1 | Son s; |
派生类赋值给基类(向上转型)
派生类赋值给基类,称为向上转型。相应地,将基类赋值给派生类,称为向下转型。
向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。
派生类对象赋值给基类对象
1
Father f = Son();
派生类指针赋值给基类指针
1
Father *f = new Son();
派生类引用赋值给基类引用
1
2Son s;
Father &f = s;
赋值只包含成员变量,不包含成员函数。所以调用同名成员函数时,f
依旧调用的是原本基类Father
的成员函数。
多态
多态,即函数多种形态,分为两类:
- 静态多态
- 函数重载、运算符重载
- 静态:编译阶段绑定函数地址
- 动态多态
- 派生类和虚函数实现运行时的多态
- 动态:运行阶段绑定函数地址
当基类和派生类中有相同成员函数时,
1 | class Animal { |
派生类调用,会遮蔽基类中的所有同名函数,直接调用派生类的函数,所以c.speak();
输出了“喵喵喵”。
派生类赋值给基类后,只改变成员变量,不改变成员函数,基类调用,依旧调用的是原本基类的成员函数,所以a->speak();
输出了“动物发声”。
1 | Cat c; |
虚函数
如果希望a->speak();
调用的是派生类函数的“喵喵喵”,则应将基类的speak()
设置为虚函数,添加关键字virtual
1 |
|
如不是虚函数,在编译时就会静态链接,将
a->speak();
早绑定到基类函数上。如是虚函数,在运行时才会动态链接,如此,便可将
a->speak();
绑定到派生类函数上。
纯虚函数
如果基类中的虚函数没有实际意义,可以定义为纯虚函数。
1 | virtual 返回值类型 函数名 (函数参数) = 0; |
纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0
,表明此函数为纯虚函数。
1 | class Animal { |
模版
C++泛型编程,主要技术就是模版。使变量变成通用变量,如vector<int>
的利用模版技术,可以输入不同的数据类型。
函数模版/类模版
定义
分为建立一个通用函数/类,返回值和形参的类型可以不具体制定,而用一个虚拟类型来表示。
函数模版例如swap
函数,输入的两个参数都是模版,所有可以交换两个int
,也可以交换两个string
。
类模版例如vector<int>
,其中的成员类型可变换。语法
1
2template <typename T>
函数/类声明或定义- template表明声明一个模版
- typename可以换成class,效果相同
- T为通用数据类型名,告诉编译器不要报错
函数模版例子
自行定义一个交换函数模版
1
2
3
4
5
6template<typename T>
void Swap(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}调用模版函数,可以自动类型推导
1
2int a = 10, b = 20;
Swap(a, b);也可以指定类型
1
2int a = 10, b = 20;
Swap<int>(a, b);类模版
定义一个模版类
1
2
3
4
5
6
7
8
9
10template<typename NameType, typename AgeType>
class Person {
NameType m_name;
AgeType m_age;
public:
Person(NameType name, AgeType age) {
m_name = name;
m_age = age;
}
};调用模版类
1
Person<string, int> p("Job", 10);
STL
STL(Standard Template Library),即标准模板库。
STL由三大组件构成:容器、算法、迭代器。
容器:特定的数据结构,如 向量(vector)、双端队列(deque)、链表(list)、集合(set)、多重集合(multiset)、映射(map)和多重映射(multimap)等
算法:常用的各种算法,如 sort、find、copy、for_each等
迭代器:检查容器内元素并遍历元素的数据类型
vector(向量)
向量(vector)是一个封装了动态大小数组的顺序容器
函数方法
代码 | 功能 |
---|---|
v.front() | 返回第一个数据 |
v.back() | 返回最后一个数据 |
v.pop_back() | 删除最后一个数据 |
v.push_back(element) | 在尾部加一个数据 |
v.size() | 返回元素个数 |
v.clear() | 清除所有元素 |
v.resize(n,v) | 改变数组大小为n,n个空间数值赋为v,如果没有默认赋值为0 |
v.insert(it,x) | 向任意迭代器it插入一个元素x |
v.erase(first,last) | 删除[first,last)的所有元素 |
v.begin() | 返回首元素的迭代器 |
v.end() | 返回最后一个元素后一个位置的迭代器 |
v.empty() | 判断是否为空,为空返回真,反之返回假 |
基本用法
运用vector容器需要引用vector头文件
1 |
- vector初始化
方法 | 描述 |
---|---|
vector |
默认初始化一个空vector |
vector |
v2中包含有v1所有元素的副本 |
vector |
等价于 v2(v1) |
vector |
n个重复的元素val |
vector |
n个重复的默认 |
vector |
赋值 |
vector |
等价于 v{a, b, c….} |
二维vector初始化
1
2// 初始化10*5的二维vector,元素值都为1
vector<vector<int>> v(10, vector<int>(5, 1));尾部插入元素
1
v.push_back(x);
插入元素
1
v.insert(v.begin() + 2, x); //在第3个位置插入x
删除元素
1
2v.erase(v.begin() + 2); //删除第3个位置的元素
v.erase(v.begin() + i, v.begin() + j); //删除第[i, j)位置的元素遍历vector集合
1
2
3
4
5
6
7
8
9//迭代器遍历访问
for(vector<int>::iterator it = s.begin(); it != s.end(); it++){
cout << *it << " ";
}
//另一种遍历访问方式
for(auto i : s){
cout << i << " ";
}
set(集合)
set(集合)的每个元素只出现一次,且默认升序排列。
函数方法
代码 | 功能 |
---|---|
s.begin() | 返回set容器的第一个元素的地址(迭代器) |
s.end() | 返回set容器的最后一个元素的下一个地址(迭代器) |
s.rbegin() | 返回逆序迭代器,指向容器元素最后一个位置 |
s.rend() | 返回逆序迭代器,指向容器第一个元素前面的位置 |
s.clear() | 删除set容器中的所有的元素,返回unsigned int类型O(N) |
s.empty() | 判断set容器是否为空 |
s.insert() | 插入一个元素 |
s.size() | 返回当前set容器中的元素个数O(1) |
erase(iterator) | 删除定位器iterator指向的值 |
erase(first,second) | 删除定位器first和second之间的值 |
erase(key_value) | 删除键值key_value的值 |
s.find(元素) | 查找set中的某一元素,有则返回该元素对应的迭代器,无则返回结束迭代器,即s.end() |
s.lower_bound(k) | 返回大于等于k的第一个元素的迭代器 |
s.upper_bound(k) | 返回大于k的第一个元素的迭代器 |
基本用法
运用set容器需要引用set头文件
1 |
set构造函数
1
2set<int> s; //默认按键值升序
set<int, greater<int>> p; //降序遍历set集合
1
2
3
4
5
6
7
8
9//迭代器遍历访问
for(set<int>::iterator it = s.begin(); it != s.end(); it++){
cout << *it << " ";
}
//另一种遍历访问方式
for(auto i : s){
cout << i << " ";
}
map(映射)
map(映射)的每个元素都是一个pair,包含 <键值,实值>,map不允许两个元素有相同的键值,所有元素根据键值自动排序。
函数方法
代码 | 功能 |
---|---|
mp.find(key) | 返回键为key的映射的迭代器,当数据存在时,返回数据所在位置的迭代器,数据不存在时,返回mp.end() |
mp.erase(it) | 删除迭代器对应的键和值 |
mp.erase(key) | 根据映射的键删除键和值 |
mp.erase(first,last) | 删除左闭右开区间迭代器对应的键和值 |
mp.size() | 返回映射的对数 |
mp.clear() | 清空map中的所有元素 |
mp.insert() | 插入元素,插入时要构造键值对 |
mp.empty() | 如果map为空,返回true,否则返回false |
mp.begin() | 返回指向map第一个元素的迭代器(地址) |
mp.end() | 返回指向map尾部的迭代器(最后一个元素的下一个地址) |
mp.rbegin() | 返回指向map最后一个元素的反向迭代器(地址) |
mp.rend() | 返回指向map第一个元素前面(上一个)的反向迭代器(地址) |
mp.count(key) | 查看元素是否存在,因为map中键是唯一的,所以存在返回1,不存在返回0 |
mp.lower_bound() | 返回一个迭代器,指向键值>= key的第一个元素 |
mp.upper_bound() | 返回一个迭代器,指向键值> key的第一个元素 |
基本用法
运用map容器需要引用map头文件
1 |
map构造函数
1
map<string, int> mp; //键值为string,实值为int
添加元素
方法一:通过数组的方式插入值
1
mp["a"] = 1; //mp[key],如果不存在对应的key时,会自动创建一个键值对
方法二:通过pair的方式插入对象
1
2
3
4
5
6
7mp.insert({"a", 1});
mp.insert(make_pair("a", 1));
mp.insert({"a",1});
mp.insert(pair<string, int> ("a", 1));
访问元素
通过下标访问
1
cout << mp["a"] << endl;
通过find函数访问
1
2map<string, int>::iterator it = mp.find("a");
cout << it->first << " " << it->second << endl;
遍历元素
通过迭代器遍历
1
2
3
4// 正向遍历(利用begin和end函数)
for(auto it = mp.begin(); it != mp.end(); ++it){
cout << it->first << " " << it->second << endl;
}1
2
3
4
5
6// 逆向遍历(利用rbegin和rend函数)
auto it = mp.rbegin();
while (it != mp.rend()) {
cout << it->first << " " << it->second << endl;
it++;
}范围for语句遍历
1
2
3
4// 只能访问,无法改变容器中的值
for(auto i : mp){
cout << i.first << " " << i.second << endl;
}1
2
3
4// 如需改变值,则用引用
for(auto &i : mp){
i.second *= 2;
}通过迭代器遍历时,迭代器
it
可以理解为指向元素的指针,指针用->
访问,即it->first
,或用(*it).first
,可改变容器中的值范围for语句遍历时,
i
是一个pair对象,直接用.
访问,即it.first
。只访问时,用auto i : mp
,当需要改变值时,用auto &i : mp
deque(双端队列)
deque(双端队列)首尾都可以插入和删除的队列。
函数方法
代码 | 功能 |
---|---|
push_back(x) | 把x压入后端 |
push_front(x) | 把x压入前端 |
back() | 访问(不删除)后端元素 |
front() | 访问(不删除)前端元素 |
pop_back() | 删除后端元素 |
pop_front() | 删除前端元素 |
erase(iterator it) | 删除双端队列中的某一个元素 |
erase(iterator first,iterator last) | 删除双端队列中(first,last)中的元素 |
empty() | 判断deque是否空 |
size() | 返回deque的元素数量 |
clear() | 清空deque |
基本用法
运用deque容器需要引用deque头文件
1 |
deque构造函数
1
deque<int> d;
stack(栈)
stack(栈),先进后出的数据结构。
函数方法
代码 | 功能 |
---|---|
s.push(x) | 将x压入栈顶 |
s.top() | 返回栈顶的元素 |
s.pop() | 删除栈顶的元素 |
s.size() | 返回栈中元素的个数 |
s.empty() | 检查栈是否为空,若为空返回true,否则返回false |
基本用法
运用stack容器需要引用stack头文件
1 |
stack构造函数
1
stack<int> s;
string(字符串)
string是C++中的一个类,专门实现字符串的相关操作。数据类型为string,字符串结尾没有\0
字符。
与之相比,C语言字符串(C-string),用char数组实现,字符串结尾以\0
结尾。
基本用法
1 |
初始化
1
2
3
4
5
6
7
8
9
10
11string str1; //生成空字符串
string str2("12345678"); //结果为"12345678"
string str3("12345678", 1, 3); //结果为"234",从1号开始,长度为3的字符串
string str4("12345678", 3); //结果为"123",从0号开始,长度为3的字符串
string str4(5, '2'); //结果为"22222",5个'2'
string str4(str2, 3); //结果为"45678",从3号开始的字符串读入
cin >> str
读入字符串,遇到空格或回车结束getline(cin, str)
,读入一行字符串,包括空格,遇到回车结束注意:
cin
输入回车结束后,回车仍在输入流中,getline
会获取前一个输入的换行符,所以需要在前面添加读取换行符的语句:getchar()
或cin.get()
错误读取方式
1
2
3string str1, str2;
cin >> str1;
getline(cin, str2); //此处getline只能读到上一个cin的换行符正确读取方式
1
2
3
4string str1, str2;
cin >> str1;
getchar(); //或者cin.get(),用于接收上一个换行符
getline(cin, str2); //此处getline只能读到上一个cin的换行符
获取长度
代码 | 含义 |
---|---|
s.size()或s.length() | 返回string对象的字符个数 |
s.max_size() | 返回string对象最多包含的字符数,超出会抛出length_error异常 |
s.capacity() | 重新分配内存之前,string对象能包含的最大字符数 |
- 插入
代码 | 含义 |
---|---|
s.push_back(element) | 在末尾插入一个字符element |
s.insert(iterator it,element) | 在迭代器it处插入一个字符element |
s.append(str) | 在s字符串结尾添加str字符串 |
1 | string s = "123456"; |
- 删除
代码 | 含义 |
---|---|
s.erase(iterator it) | 删除字符串中it所指的字符 |
s.erase(iterator first, iterator last) | 删除字符串中迭代器区间[first,last)上所有字符 |
s.erase(pos, len) | 删除字符串中从索引位置pos开始的len个字符 |
s.clear() | 删除字符串中所有字符 |
1 | string s = "123456789"; |
- 字符替换
代码 | 含义 |
---|---|
s.replace(pos,n,str) | 把当前字符串从索引pos开始的n个字符替换为str |
s.replace(pos,n,n1,c) | 把当前字符串从索引pos开始的n个字符替换为n1个字符c |
s.replace(iterator first,iterator last,str) | 把当前字符串[first,last)区间替换为str |
1 | string s = "123456789"; |
- 分割
代码 | 含义 |
---|---|
s.substr(pos,n) | 截取从pos索引开始的n个字符 |
1 | string s = "123456789"; |
- 查找
代码 | 含义 |
---|---|
s.find (str, pos) | 在当前字符串的pos索引位置(默认为0)开始,查找子串str,返回找到的位置索引,-1表示查找不到子串 |
s.find (c, pos) | 在当前字符串的pos索引位置(默认为0)开始,查找字符c,返回找到的位置索引,-1表示查找不到字符 |
s.rfind (str, pos) | 在当前字符串的pos索引位置开始,反向查找子串s、str,返回找到的位置索引,-1表示查找不到子串 |
s.rfind (c,pos) | 在当前字符串的pos索引位置开始,反向查找字符c,返回找到的位置索引,-1表示查找不到字符 |
s.find_first_of (str, pos) | 在当前字符串的pos索引位置(默认为0)开始,查找子串str的字符,返回找到的位置索引,-1表示查找不到字符 |
s.find_first_not_of (str,pos) | 在当前字符串的pos索引位置(默认为0)开始,查找第一个不位于子串s的字符,返回找到的位置索引,-1表示查找不到字符 |
s.find_last_of(str, pos) | 在当前字符串的pos索引位置开始,查找最后一个位于子串s的字符,返回找到的位置索引,-1表示查找不到字符 |
s.find_last_not_of (str, pos) | 在当前字符串的pos索引位置开始,查找最后一个不位于子串s的字符,返回找到的位置索引,-1表示查找不到子串 |
1 | string s = "This is a string."; |
- 排序
1 | string s = "349725618"; |
内存分配
C++ 内存分为以下几个部分:
栈区(stack)
- 存放函数的参数值、局部变量、返回值、返回地址等
- 由编译器自动分配和释放
堆区(heap)
- 存放动态分配的内存,如
new
、malloc
分配的动态变量 - STL(除
pair
)也都是存放在堆区 - 堆区大小不固定,由程序手动分配和释放
- 存放动态分配的内存,如
全局区/静态区(static)
- 存放全局变量、静态变量
static
- 分为
data
段和bss
段,已初始化的全局变量和静态变量存放在data
段,未初始化或者初始化为0的全局变量和静态变量存放在bss
段 - 程序启动时被分配,直到程序结束时自动释放
- 存放全局变量、静态变量
常量区
- 存放常量,不允许修改
- 程序启动时被分配,直到程序结束时自动释放
代码区
- 存放指令代码
- 程序启动时被分配,直到程序结束时自动释放
1 | int a1; //全局区的bss段 |
类型转换
隐式转换
隐式转换,即在类型不统一时,系统自动进行的类型转换
何时发生隐式转换?
- 算术运算中,低类型转换为高类型
- 赋值表达式中,右边的表达式的值自动转化为左边变量的类型
- 函数传参时,将实参转化为形参的类型
- 函数返回时,将返回表达式转化为返回值的类型
算数转换
算数转换是隐式转换的一种,会将低类型转换为高类型
显式转换
常见问题记录
作为函数参数的多维数组
详见《C和指针》P159
传数组参数,即是要传递指向数组第一个元素的指针。
以一维数组为例,vector
即为指向数组第一个int
元素的指针
1 | int vector[10]; |
参数vector
是指向int
型的指针,所以函数定义可以是如下两种方法
1 | void func1(int *vector); |
多维数组传参,同样是传指向第一个元素的指针,但有所不同的是,多维数组的每个元素本身也是另一个数组,编译器需要知道它的维度。
以二维数组为例,二维数组matrix[3][10]
相当于是包含3个元素的一维数组,每个元素又是一个包含10个元素的一维数组,matrix
的类型是指向包含10个整型元素的数组的指针。
1 | int matrix[3][10]; |
所以,函数的原型必须包含第二个维度10,编译器才知道什么时候开始换行,可以有如下两种定义方式:
1 | void func1(int (*matrix)[10]); |
关键就在于编译器必须知道第2个及以后各维的长度,才能对下标进行求值
典型的错误写法
错误写法一:未包含第二个维度长度,
**matrix
为指向整型指针的指针,而不是指向数组的指针1
void func2(int **matrix);
错误写法二:
*matrix[10]
为指针数组,即数组元素是指针;(*matrix)[10]
为数组指针,即指向数组的指针1
void func2(int *matrix[10]);
类模板头文件的编写
c++中模板的声明和定义不能分开
问题描述
如和一般的类头文件一样,将模板类中函数声明写在类名.hpp
中,函数定义写在类名.cpp
中,类名.cpp
、main.cpp
调用.h
文件,则会出现报错:
1 | error: Undefined Symbol 成员函数 |
问题分析
模板类中的成员函数在调用时才创建。
C++编译时,就要确定每个对象的空间大小。
但是,模板类在未被使用前,无法确定大小,比如
vector<int>
和vector<char>
,这两套用不同数据类型的模版,实际是两个不同的类。
所以,c++中模板的声明和定义不能分开。
解决方法
方法一:将模板类中的成员函数的声明和定义都写在
.h
文件里Stack.hpp++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//顺序栈
template <typename T>
class Stack {
T *data; //成员数组
int top; //栈顶指针,当前栈顶元素的位置
int size; //栈中元素的最大个数
public:
Stack();
~Stack();
};
//栈初始化
template <typename T>
Stack<T>::Stack() : top(-1), size(10) {
data = new T[size];
}
//销毁栈
template <typename T>
Stack<T>::~Stack() {
delete [] data;
data = nullptr;
}在
main.cpp
调用Stack.hpp
文件方法二:
main.cpp
调用类名.cpp
文件,类名.cpp
文件调用类名.h
文件Stack.hpp++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//顺序栈
template <typename T>
class Stack {
T *data; //成员数组
int top; //栈顶指针,当前栈顶元素的位置
int size; //栈中元素的最大个数
public:
Stack();
~Stack();
};Stack.hpp++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
//栈初始化
template <typename T>
Stack<T>::Stack() : top(-1), size(10) {
data = new T[size];
}
//销毁栈
template <typename T>
Stack<T>::~Stack() {
delete [] data;
data = nullptr;
}在
main.cpp
调用Stack.cpp
文件
常用函数
sort()排序函数
sort()函数类似于快速排序,时间复杂度为 $n*log2(n)$
- 头文件
1
- 基本使用方法
1
sort(begin, end, cmp);
begin
:待排序的数组的第一个元素的指针end
:待排序的数组的最后一个元素的下一个位置的指针cmp
:排序准则,不填则默认为从小到大排序,如想要从大到小排序,则填greater<int>()
。如需自行定义排序准则,也可传入bool型函数,返回true
则不换位置,返回false
则前后调换位置
- 用例
- 数组排序
1
2
3int num[5] = {4, 3, 2, 1, 0};
sort(num, num+5); //从小到大排序:0 1 2 3 4
sort(num, num+5, greater<int>()); //从大到小排序:4 3 2 1 0 - vector排序
1
2vector<int> v = {4, 3, 2, 1, 0};
sort(v.begin(), v.end()); //从小到大排序:0 1 2 3 4 - 自定义排序准则
1
2
3
4
5
6
7
8
9//个位数从大到小排序
bool cmp (int x, int y) {
return x % 10 > y % 10; //x个位大于y时,返回true
}
int main () {
int num[5] = {24, 1, 83, 12, 30};
sort(num, num + 5, cmp); //个位数从大到小排序:24 83 12 1 30
}
- 数组排序