C转C++教程
Why C++
经过大一一整年的学习,相信各位的C语言都有了扎实的基础——。
但是咱们课程不讲C++,但是咱的算法题很多用C++会变得更方便,所以还得会。
于是笨人最近在学习C++,故写进博客。
先来看一段简单的代码
1 |
|
嗯,很显然这是一个输入n,然后输出Hello World再输出n的程序
我们先来看第1行#include<iostream>
其实就是跟C语言一样导入库,只不过导入的不是<stdio.h>库了,<iostream>可以认为是一个输入输出流的库。
接下来再康第三行
1 | using namespace std; |
的意思是 使用 命名空间 std。
这地方要说可能要说挺多。结论是:现在做算法题和小程序建议加上这一句,解释不想看的可以快进(嗯嗯。
下面是解释
命名空间?
命名空间是什么
在C++中,命名空间(namespace)是一种用来防止名字冲突的机制。
如:
1 | int count = 10; |
如果写了很多个库,每个库里都有一个count变量,那编译器就不知道该用哪个了。
于是为了解决这种问题,C++把不同库里的名字放进不同的命名空间里。
如
1 | namespace A{ |
通过这样,就可以在使用时,区分两个count,如
1 | A::count += 1; |
就把两个命名空间里的count区分开了。
std命名空间
C++ 标准库里的所有东西(比如 cout, cin, string, vector 等等)
都被放进了一个叫 std(standard) 的命名空间里。
所以严格写法是
1 |
|
这里的std:: 表示从命名空间std里找cin,cout,endl
using namespace std;的作用
这条语句的意思是:从现在开始,使用命名空间std里的所有名字时,不需要加前缀std::
起到了一个省略的作用
于是就变成了最上面的那个代码的样子
有时不建议使用它
using namespace std的写法,在小程序里非常方便。
但如果在大型项目和库开发中,就可能引发命名冲突。
如自己写了一个
1 | void sort(){/* ... */} |
但标准库里也有一个std::sort()
所以,如果用了using namespace std;,编译器就可能混淆这两个函数。
所以最安全的做法就是不用这个语句,干脆把所有的std::前缀都加上。
cin, cout
就是C++的输入和输出函数,在一定条件下可以代替scanf,printf函数(不过效率赶不上)。
使用cin的时候就不用加取地址符&了
注意两个函数使用时的箭头方向>> <<
若有多个输入和输出的元素,可以用多个箭头链接,如
1 | cin >> a >> b >> c; |
endl
end of line的简写,用来代替换行。
当然也可以用\n替代
bool
再来看一个简单的代码
1 |
|
输出的结果为1 1 0
bool只有两个取值,true和flase
任何非0的值,都会被转成true,只有0会被转成flase。
提示:bool虽然只有0和1两个值,可以用二进制的1位表示,但是bool变量实际是占1个字节(8位)。
为什么?
因为:
计算机的最小可寻址单位是字节(byte)。
CPU 不能直接寻址“半个字节”或“1 bit”的内存单元,
所以即使只有一个布尔值,也得分配整整 1 个字节。
string类
字符串的定义
1 | string s1 = "hello"; |
so easy~
字符串的更改
和C语言一样,可以通过使用
1 | s[0] = 'H'; |
这种方式更改
字符串的拼接
1 | s3 = s1 + s2; |
直接用’+’就可以链接字符串了,so easy~。
字符串的输入
这里可以用C语言类比一下。
使用
1 | cin >> s |
就可以把输入字符串了,其中cin会在读取到空格、回车时停止,和scanf相同。
那当然还有输入一行的了~
1 | getline(cin, s); |
这里的cin是指从标准输入流(键盘)中读入字符串,存到字符串s里。
字符串的输出
1 | cout << s; |
so easy~
字符串的长度
所求字符串s的长度,这个跟C语言有些不同
使用
1 | int len = s.length(); |
即可把字符串s的长度存到len中
为什么要这么写而不是像strlen(s)一样,把字符串放到函数里呢?
让我们回看这一节的标题,字符串类,没错他是一个类。那么学过java的都知道了,类是有属性和方法的,这里的length()就是一个方法,返回这个字符串的长度。
字符串的复制
可以直接用 = 来赋值,如
1 | s1 = 'hello world'; |
字符串其他常用函数
s.empty()
判断s是否为空,空返回1,否则返回0。
s.front()
返回s第一个字符
s.back()
返回s最后一个字符
s += t / s.append(t)
拼接字符串
s.push_back(c)
添加单个字符到末尾
s.pop_back()
删除末尾字符
s.clear()
清空字符串
s.find(t)
在字符串s里找字符串t首次出现位置
s.substr(pos, len)
获取s中pos处,长度为len的字符串
s.substr(pos)
如果只输入一个值的话,就是获取从pos处到字符串s末尾的子串
s.compare(t)
把s和t比较,等同于strcmp(s, t)等同于
s1 == s2
判断两个字符串是否相等,若相等返回true,否则false。
不常用但有时会用的函数
s.insert(pos, t)
在指定位置插入字符串
s.erase(pos, len)
删除子串
s.replace(pos, len, t)
替换子串
s.swap(t)
交换两个字符串
结构体
C++的结构体使用和C有些不同
看这个例子
1 | struct stu{ |
比如我们要生成一个stu的数组,在C中我们需要写
1 | struct stu a[10]; |
而在C++中,我们只需要写
1 | stu a[10]; |
看出不同来了吧~,也就是说在这里我们不需要使用typedef了~
&的使用
看下面这个例子
1 |
|
通过这种方法,可以直接实现a += 1,即输出3
int &a 表示 引用,也就是,别名
当你传入 a 时,函数内部的 a ,实际上就是 main 中的 a
对它的修改会直接影响原变量
也就是直接把形参变成了实参~。
vector
vector是啥?向量?彳亍。更精确的说,是可变数组——
(以下简称数组)
创建数组、分配数组大小
让我们先来看一段代码~
1 |
|
这啥东西啊我怎么忽然就看不懂了
这段代码的输出是
1 | 0 |
先看代码的第2行,使用vector之前,需要先导入vector库,即需要#include<vector>
how to创建数组??
我们只需要通过第8行中vector <int> v就可以创建一个int类型的数组了!
那这个数组的大小的是多大呢,我们可以通过调用v.size()并且输出看一下,这个方法会返回数组v的大小。
于是乎我们看到了,输出结果第一行是0,也就是说咱创建的数组长度是0。
长度是0像话吗。咱们肯定需要更改一下这个数组的长度,这时只要通过使用v.resize(length)
既可以重新分配数组的大小了,再输出,我们就看到原数组的大小改变了。
创建数组还有三种方式,分别是
1 | vector <int> v(10, 2); |
这两种方式与第一种方式的参数数量不同。vector <int> v(10, 2);作用就是创建一个长度为10的数组v,并且初始化数组中每个元素都是2。
也就是说省去了resize的过程,并且顺便初始化了整个数组。
而vector <int> u(10),其实等价于vector <int> u(10)
即把数组u中每个元素都初始化成0。
至于vector<int> v = {1, 2, 3, 4, 5, 6},也就是和C语言一样手动初始化数组
另外 v.resize(n, val)实际也有两个参数
当缺少参数val时,只更改数组大小(如果之前小,新的就补0,之前大就删掉
当加上参数val时,则新加的元素会自动初始化成val,但是如果之前的长度比更改后的长度n长,那么参数val就会被忽略。
更改数组元素
这里用法与C语言相同,如使用v[0] = 10就可以直接更改了。
末尾添加新的元素
通过使用
1 | v.push_back(data); |
就可以在v后追加元素
例如
1 |
|
输出的结果是
1 | 0 0 0 0 0 0 0 0 0 0 11 |
我们发现,前十个初始化成0的元素并没有被改变,而是多出了第11个元素。
也就是说,pushback(data)会把data这个值放在原来的数组后面,也就是更改了数组长度。
此时再使用
1 | cout << v.size(); |
会发现数组的长度变成了11。
迭代器
我们看一段代码
1 |
|
输出的结果是
1 | 2 2 2 2 2 2 11 12 |
看到这个for循环和输出我们就猜到了,这个迭代器的作用说白了就是遍历。
auto p = v.begin(),也就是让迭代器p指向v的第一个元素。
为什么说指向?也就是说vector这里的p就是一个指针。
略微不同之处是,我们不用关心它的类型,即使用auto p就可以创建迭代器了。
p != v.end() 也就是p还不等于的end处。注意,这里的p.end()不是最后一个元素喔,是最后一个元素的==下一个元素==。
也就是说!
1 | 2 2 2 2 2 2 11 12 |
这也保证了v.size() = v.end() - v.begin();
一些底层逻辑
不关心的可以跳过(
<vector>的底层逻辑,其实就是数组,占用一块连续的内存,其内部维护了三块指针,分别是
1 | T* _begin; // 指向数据开头 |
对应示意图
1 | | 元素1 | 元素2 | 元素3 | 空 | 空 | 空 | |
这样有
size() = _end - _begincapacity() = _capEnd - begin
这里出现了一个新的函数,v.capacity(),表示vector最多能容纳多少个元素,而不用再次分配内存。也就是说,v.size()只是数组的长度,而不是内存的长度。而真正的内存长度会由_begin和_capEnd两个指针共同决定。
初始化数组和使用resize()更改数组长度时,会根据长度自动分配数组占用内存。此时size和capacity相等。
当使用v.push_back(num)后,会检测当前数组占用的内存是否都被利用了,如果没有,即end!=capEnd,此时不需要更改内存大小,直接后移end指针即可。
但如果end == capEnd,此时内存不够,就需要扩容了。
但我们知道数组占用的内存是连续的,扩容的话必须再找另外一块更大的连续的内存了。
所以此时,会重新分配一块更大的空间,并且把原来的数据全部拷贝过去,再释放旧内存。
但是为了避免多次v.push_back()扩容反复拷贝浪费时间,这里采用指数级增长,即让新的capacity是原来的两倍,降低拷贝次数,节省时间。
而为了减少频繁扩容带来的开销,可以手动提前“预留空间”
与v.resize()类似,即使用v.reserve(1000); 就可以手动更改预留内存空间。
set
set是什么?集合!
创建和插入
set中的元素有两个特性
set中的元素是互异的。set中的元素会自动排好序
来看代码
1 |
|
第二行,使用set前,要先导入set库,即#include<set>
set创建和vector类似,通过set <type> set_name方式进行创建,如代码的第七行。
有一点不同是,set_name后是不带length和val参数的。
即set <int> s(10);
这种方式不能通过编译。
也很好理解,集合这个东西,应该是要随时加元素和删元素的,为什么要分配空间呢。
但是如果想创建同时初始化集合,使用set <int> s = {1, 2, 3};
这种方式是阔以的。
想在集合中插入元素,只需要通过s.insert(val);
就可以~
在上面的代码中,我们先后试图插入了1, 3, 2, 1四个元素,随后通过迭代器输出集合s中的元素。
这段代码运行的结果是1 2 3
没戳,正如这一节一开始说的那样,集合是有互异性的,并且set中的元素是会被自动排序(默认升序)的。
那怎么让它降序呢?只需在创建set时这么写~set <int, greater<int>> ss就会定义成一个默认降序的集合了~
集合大小
使用s.size()就可以~
元素查找
如何查找集合中的元素呢,只需使用s.find(val)
若集合s中存在val这个元素,会返回val这个元素的==位置==
但如果不存在,会返回s.end()这个位置!而不是NULL
如
1 | set <int> s = {1, 2, 3}; |
输出的结果是
1 | 0 |
元素删除
只需要通过s.erase(val)就可以把集合中val这个元素删掉了~
如果s中没有val这个元素,这行运行之后将无事发生。
map
map是什么?键值对!也就是python里的字典——
什么是键值对?也就是一个值映射一个值,其实也就是哈希表
创建和插入
我们来看代码
1 |
|
使用map前,要先导入map库
创建map时,只需使用map <key, val> m就可以创建一个map了,这里的key和val都不限制类型。
这里的key就是“键”,val就是“值”。
如何添加一个键值对?
只需要使用类似上面代码8,9行的代码。使用m[key] = val进行赋值操作
若map中不存在那个键是key的键值对,就会创建<key, val>这个键值对
如果已经存在一个键就是key的键值对<key, val>了,那么赋值操作就会更改那个val值
访问
如何访问键值对?只需要使用m[key]就会返回<key, val>键值对中val的值了
上面那段代码的输出是
1 | hello : 114514 |
那如果不存在这个键值对呢,程序会自动插入一个键值对<key, val>,并且把val是默认初始化的值(如int初始化成0)
比如这段代码
1 |
|
的输出将是
1 | hello : 114514 |
查找
有时,我们只是想查找或者访问已经存在的键值对,而不想在访问键值对的同时就把不存在的键值对也加进去。对此,我们只要调用m.find(key);就可以~
比如
1 | if(m.find("hello") != m.end()){ |
再次提醒。STL里的
find()函数,若查找失败,返回的都是end(),而不是空指针!
迭代器
在使用map的迭代器时,需要注意要通过
1 | for(auto p = m.begin(); p != m.end(); p++){ |
这种方式。用p -> first来访问“键”,用p -> second来访问“值”。
直接访问*p会编译错误
这里的用法和指针类似,很好理解~
unordered_set and unordered_map
其实就是不会在插入时自动排序的set和map
使用方法完全和set和map相同
由于不会自动排序,所以节约了时间~如果使用set和map时TLE,可以考虑使用这两个unordered_set 和unordered_map。
不过无序,并不代表使用迭代器输出时,输出顺序就是插入顺序——绝大概率是乱序的!
一些底层逻辑
set和map的实现用的是红黑树(一种特殊的二叉查找树),保证了插入并排序、查找,和删除的时间复杂度都是O(logn),很快~
推荐一个B站上讲解红黑树的可视化视频
而unordered_set和unordered_map是用哈希表和链表实现的,所以更快,也能解释为什么输出顺序是乱序的~
stack用法
使用前先导入#include<stack>
然后创建stack <type> s;
然后一堆操作
压栈s.push(i);
出栈s.pop();
访问栈顶s.top();
获取长度s.size();
判断是否为空s.empty();
想必从第一节看过来的不用解释都能看懂啦~
不能使用迭代器(栈怎么能遍历呢?)
queue用法
使用前先导入#include<queue>
然后创建queue <type> q;
然后一堆操作~
入队q.push(i);
出队q.pop();
访问队首q.front();
访问队尾q.back();
获取长度q.size();
判断是否为空q.empty();
想必从第一节看过来的不用解释都能看懂啦~
不能使用迭代器(队列怎么能遍历呢?)
priority_queue用法
优先队列!堆!!
使用前先导入#include<queue>
然后创建priority_queue <type> pq;
(默认是一个大根堆)
如果想要一个小根堆呢?
只需要这么写priority_queue<int, vector<int>, greater<int>> pq;
==注意,<>里的三个参数缺一不可==
一些操作
插入堆pq.push(i);
删除堆顶元素pq.pop();
访问堆顶元素pq.top();
判断是否为空pq.empty();
返回元素数量pq.size();
想必从第一节看过来的不用解释都能看懂啦~
不过不能直接堆排序~,想输出排序结果的话,可以使用
1 | while(!pq.empty()){ |
就可以啦~
不能使用迭代器(堆怎么能遍历呢)
STL暂时告一段落啦~~~
- Title: C转C++教程
- Author: Connor
- Created at : 2025-10-14 13:13:28
- Updated at : 2026-06-30 23:53:47
- Link: https://redefine.ohevan.com/2025/10/14/CtoC++/
- License: This work is licensed under CC BY-NC-SA 4.0.