⭐️C++入门基础精讲(一):从发展历史到第一个程序
0.1前言
初学C++很多人对于C++是什么,如何学好C++有很多困惑。本篇文章将从发展历史开始,系统完整地讲解C++入门阶段的核心知识点。以真诚换真心,倾尽全力做到最好。还请大家多多三连支持!非常感谢!
0.2概述
C++是C++之父本贾尼博士在C语言的基础上创造出来的一种更为完善的计算机语言,解决了C语言的很多漏洞与缺陷。是现代社会生活中最重要的计算机语言之一。
一、C++发展历史
1.1起源
C++的起源可以追溯到1979年,当时Bjarne Stroustrup(本贾尼·斯特劳斯特卢普)在贝尔实验室从事计算机科学和软件工程的研究工作。面对项目中复杂的软件开发任务,特别是模拟和操作系统的开发工作,他感受到了现有语言(如C语言)在表达能力和可维护性方面的不足。
1.2正式命名
1983年 ,Bjarne Stroustrup在C语言的基础上添加了面向对象编程的特性,设计出了C++语言的雏形,此时的C++已经有了类、封装、继承等核心概念,为后来的面向对象编程奠定了基础。这一年该语言被正式命名为C++。
1.3标准化之路
C++的标准化工作于1989年开始,并成立了ANSI和ISO(International Standards Organization)国际标准化组织的联合标准化委员会。
1994年标准化委员会提出了第一个标准化草案。
STL(Standard Template Library)是惠普实验室开发的一系列软件的统称。它是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的。在通过标准化第一个草案之后,联合标准化委员会投票并通过了将STL包含到C++标准中的提议。
1997年11月14日,联合标准化委员会通过了该标准的最终草案。
1998年,C++的ANSI/ISO标准被正式投入使用。
1.4版本更新一览
| 版本 | 年份 | 重要特性 |
|---|---|---|
| C++98 | 1998 | 第一个ISO标准,包含STL |
| C++03 | 2003 | 修正bug |
| C++11 | 2011 | Lambda、范围for、智能指针 |
| C++14 | 2014 | 泛型lambda、二进制字面量 |
| C++17 | 2017 | 结构化绑定、if constexpr |
| C++20 | 2020 | 概念、协程、模块 |
| C++23 | 2023 | 进一步完善标准库 |
1.5关于C++23的小故事
C++一直被诟病的一个地方就是一直没出网络库(networking),networking之前是在C++23的计划中的,现在C++23已经发布了,但是没有networking,网上引发了一系列的吃瓜和吐槽。中间过程就像发生了宫斗剧一样。
二、C++参考文档
推荐三个常用的C++参考网站:
| 网站 | 说明 |
|---|---|
| legacy.cplusplus.com | 不是官方文档,标准只更新到C++11,但以头文件形式呈现,内容比较好理解 |
| zh.cppreference.com | C++官方文档的中文版,信息很全,更新到最新标准 |
| en.cppreference.com | C++官方文档的英文版,信息最全 |
三、C++的重要性与应用领域
3.1编程语言排行榜
TIOBE排行榜是根据互联网上有经验的程序员、课程和第三方厂商的数量,并使用搜索引擎(如Google、Bing、Yahoo!)以及Wikipedia、Amazon、YouTube和Baidu统计出排名数据,只是反映某个编程语言的热度,并不能说明一门编程语言好不好。
3.2C++应用领域
C++的应用领域非常广泛,可以说无处不在:
3.2.1大型系统软件开发
如编译器、数据库、操作系统、浏览器等等。
3.2.2音视频处理
常见的音视频开源库和方案有FFmpeg、WebRTC、Mediasoup、ijkplayer,音视频开发最主要的技术栈就是C++。
3.2.3PC客户端开发
一般是开发Windows上的桌面软件,比如WPS之类的,技术栈的话一般是C++和QT。QT是一个跨平台的C++图形用户界面(Graphical User Interface,GUI)程序。
3.2.4服务端开发
各种大型应用网络连接的高并发后台服务。这块Java也比较多,C++主要用于一些对性能要求比较高的地方。如:游戏服务、流媒体服务、量化高频交易服务等。
3.2.5游戏引擎开发
很多游戏引擎就都是使用C++开发的,游戏开发要掌握C++基础和数据结构,学习图形学知识,掌握游戏引擎和框架,了解引擎实现,引擎源代码可以学习UE4、Cocos2d-x等开源引擎实现。
3.2.6嵌入式开发
嵌入式把具有计算能力的主控板嵌入到机器装置或者电子装置的内部,通过软件能够控制这些装置。比如:智能手环、摄像头、扫地机器人、智能音响、门禁系统、车载系统等等,粗略一点,嵌入式开发主要分为嵌入式应用和嵌入式驱动开发。
3.2.7机器学习引擎
机器学习底层的很多算法都是用C++实现的,上层用python封装起来。如果你只想准备数据训练模型,那么学会Python基本上就够了,如果你想做机器学习系统的开发,那么需要学会C++。
3.2.8测试开发
每个公司研发团队,有研发就有测试,测试主要分为测试开发和功能测试,测试开发一般使用一些测试工具(selenium、Jmeter等),设计测试用例,然后写一些脚本进行自动化测试、性能测试等,有些还需要自行开发一些测试工具。功能测试主要是根据产品的功能,设计测试用例,然后手动的方式进行测试。
四、C++学习建议与书籍推荐
4.1学习难度
首先第一个问题,C++难学吗?C++是一个相对难学难精的语言,相比其他一些语言,学习难度要高一些、陡峭一些,这里有历史包袱的问题,也有语言本身设计和发展历史的问题。
4.2学习建议
- 勤动手:建议每节课下来把课堂讲过的课堂样例都要练习一遍,理解对应的知识。
- 多总结:建议如果时间能跟上最好每节课总结博客或笔记,如果时间少,至少重点章节整理笔记。
4.3经典书籍推荐
| 书名 | 主要内容 | 推荐阶段 |
|---|---|---|
| C++ Primer | 主要讲解语法,经典的语法书籍 | 前后中期都可以看,前期自学看可能会有点晦涩难懂,就当预习,学了之后中后期作为语法字典非常好用 |
| STL源码剖析 | 主要从底层实现的角度结合STL源码,庖丁解牛式剖析STL的实现,是侯捷老师的经典之作 | 可以很好的帮助我们学习别人用语法是如何实现出高效简洁的数据结构和算法代码,如何使用泛型封装等。课程上一半以后,中后期可以看 |
| Effective C++ | 本书也侯捷老师翻译的,本书有一句评价:把C++程序员分为看过此书的和没看过此书的。本书主要讲了55个如何正确高效使用C++的条款 | 建议中后期可以看一遍,工作1-2年后再看一遍,相信会有不一样的收获 |
五、C++的第一个程序
5.1C语言版本
C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行。C++中需要把定义文件代码后缀改为.cpp,vs编译器看到是.cpp就会调用C++编译器编译,linux下要用g++编译,不再是gcc。
cpp
// test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
5.2C++版本(推荐)
C++有一套自己的输入输出,严格说C++版本的hello world应该是这样写的:
cpp
// test.cpp
// 这里的std、cout等我们都看不懂,没关系,下面我们会依次讲解
#include<iostream>
using namespace std;
int main()
{
cout << "hello world\n" << endl;
return 0;
}
5.3编译运行
将上述代码保存为test.cpp,在Linux环境下使用以下命令编译运行:
bash
g++ test.cpp -o test
./test
输出结果:
hello world
六、命名空间
6.1解决的C语言问题:命名冲突
C/C++语言为了避免编译歧义,规定:
(1)在同一个域内,不能出现两个相同的命名。
(2)在不同的域,允许出现两个相同的命名。
在公司项目开发中,不同的项目组在命名时往往会产生大量的命名冲突,此时就不可避免的要使用大量的条件编译,极大地拖慢了业务效率。
6.1.1实例演示
示例一
cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:error C2365: "rand": 重定义;以前的定义是"函数"
printf("%d\n", rand);
return 0;
}
看代码,stdlib.h头文件中有rand函数(生成随机数)的定义,此时我们再定义一个rand变量,这里rand是全局变量的,而且# include<stdlib.h>里面库也有rand函数,库里的函数也是全局函数,所以他和我定义的rand冲突了,因为他都是同一个域的,名字相同违背了逻辑,所以不能用,这里我再举一个例子强化一下,域同名的概念。
示例二
这就不冲突。
c
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 定义局部变量rand,仅属于main函数局部作用域
int rand = 666;
printf("%d", rand);
return 0;
}
不冲突原理
-
全局:存在库函数 rand()
-
局部:存在自定义变量 rand
-
两个不在同一个作用域,规则允许重名
-
名字查找遵循就近原则:优先找当前局部域,找到就停止访问全局域
-
局部变量直接屏蔽全局同名函数,互不干扰
示例三
若头文件中 rand 是宏定义,不分全局局部,预处理直接全部替换,一律报错。
c
#define rand 999 // 宏定义,预处理全局文本替换
int main()
{
int rand; // 直接被替换成 int 999; 语法错误
return 0;
}
为什么报错
-
999是数字常量,不能当变量名
-
C语言规定:变量名必须是标识符,不能是纯数字
-
语法直接非法,编译器直接报错
总结
-
普通库函数属于全局作用域
-
全局变量和全局函数同名:同域冲突报错
-
局部变量和全局函数同名:异域屏蔽,正常使用
-
宏定义不受作用域限制,预处理优先替换,全局局部都会出错
-
起名规范:尽量避开库函数名,从根源避免冲突
6.2定义命名空间
6.2.1原理
我们为了避免域的作用规则,导致很多变量有问题,这里我们引入域的概念。
方法论 :定义命名空间需要使用到namespace关键字,后面跟命名空间的名字然后加{},{}中的就是命名空间成员。命名空间可以定义变量/函数/类型等等。
世界观 :namespace本质上定义了一个域,这个域和全局域互相独立,不同的域可以定义同名变量,与全局域的函数rand的命名不再冲突。
cpp
#include<stdio.h>
#include<stdlib.h>
namespace bit
{
int rand = 10;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
// 这里默认是访问的是全局的rand函数指针
printf("%p\n", rand);
// 这里指定bit命名空间中的rand
printf("%d\n", bit::rand);
return 0;
}
6.2.2域访问限定符
(1)符号 :::域作用限定符号是两个冒号。
(2)使用 :::的左边是限定的域,右边是访问的变量/函数等。
(3)访问全局域 :::左边什么都不写,表示访问全局域。
(4)访问命名空间 :::如果左边写一个域名,就会去这个域里面找。
6.2.3实例应用
实例一:对变量而言
cpp
namespace bit
{
int rand = 10;
}
int main()
{
// 访问命名空间bit中的rand
printf("%d\n", bit::rand); // 输出10
// 访问全局rand(如果有的话)
printf("%d\n", ::rand); // 访问全局域
return 0;
}
这里如果::前面什么都不加,强制输入全局.这里我为大家再总结一下这个的规则。
c
#include <stdio.h>
#include <stdlib.h>
// 全局变量 rand
int rand = 100;
namespace bit
{
// 命名空间内的 rand
int rand = 10;
}
int main()
{
// 局部变量 rand(和全局、命名空间里的都重名)
int rand = 999;
// 1. 直接写 rand:就近原则,访问的是局部变量 rand
printf("局部rand: %d\n", rand); // 输出 999
// 2. 加 bit::rand:访问命名空间bit里的rand
printf("bit::rand: %d\n", bit::rand); // 输出 10
// 3. 加 ::rand:强制访问全局作用域的rand,局部变量完全被跳过
printf("::rand: %d\n", ::rand); // 输出 100
return 0;
}
是不是一目了然。
实例二:对函数而言
cpp
namespace bit
{
int Add(int left, int right)
{
return left + right;
}
}
int main()
{
// 调用命名空间bit中的Add
int result = bit::Add(1, 2);
printf("%d\n", result); // 输出3
return 0;
}
实例三:对结构体而言
cpp
namespace bit
{
struct Node
{
struct Node* next; // 不需要在Node前加bit::
int val;
};
}
// 使用时需要加bit::
bit::Node n;//这里还可以写struct bit::Node
n.val = 10;
注意:结构体与函数和变量稍有不同。规定不在struct Node前加域访问限定符,而是在结构体名称前加。因为真正封装的结构体变量是Node而不是struct。
6.3命名空间嵌套定义与域合并
6.3.1为什么引入嵌套定义
公司中,每一个部门会有多个项目组。项目经理会规定不同的项目组使用不同类型的命名来避免冲突。但是同一个项目组中,两个不同的个人又可能存在命名空间冲突,于是常常采用嵌套的命名空间。
cpp
namespace bit
{
// 张三
namespace pg
{
int rand = 1;
int Add(int left, int right)
{
return left + right;
}
}
// 李四
namespace hg
{
int rand = 2;
int Add(int left, int right)
{
return (left + right) * 10;
}
}
}
int main()
{
printf("%d\n", bit::pg::rand); // 输出1
printf("%d\n", bit::hg::rand); // 输出2
printf("%d\n", bit::pg::Add(1, 2)); // 输出3
printf("%d\n", bit::hg::Add(1, 2)); // 输出30
return 0;
}
这里就是为了避免同一个项目组里面,每个人变量冲突,这里我用一个世界例子为大家在表示一下。
cpp
// 第1层:公司级大域,包住所有部门代码
namespace company
{
// 第2层:技术部门域
namespace dept_tech
{
// 第3层:后端项目组域
namespace backend
{
// 张三写的rand变量
int rand = 1;
int Add(int a, int b)
{
return a + b;
}
}
// 第3层:前端项目组域
namespace frontend
{
// 李四也写了rand变量,和后端的rand同名,完全不冲突
int rand = 100;
}
}
// 第2层:产品部门域(和技术部门的所有名字都隔离)
namespace dept_product
{
int rand = 999; // 和技术部门的rand也不冲突
}
}
int main()
{
// 访问技术部门-后端组的rand
printf("后端组rand: %d\n", company::dept_tech::backend::rand); // 输出1
// 访问技术部门-前端组的rand
printf("前端组rand: %d\n", company::dept_tech::frontend::rand); // 输出100
// 访问产品部门的rand
printf("产品部门rand: %d\n", company::dept_product::rand); // 输出999
return 0;
}
这样既可以避免多公司合作,出现命命错误,也可以避免一个公司的多个部门冲突,也可以避免,部门的,不同分块冲突,所以域很重要,这也是和C语言的区别。
6.3.2嵌套命名空间的内容调用
先在bit下面找到李四//张三,再在李四/张三下面找到我想要的变量。
6.3.3域合并
多个被定义的位于不同位置的命名空间会进行逻辑合并,即使他们在不同的文件中。即:多个文件可以只创建一个命名空间,会认为是一个命名空间。,这里说的话大家也不是很明白,我还是喜欢举例。
cpp
// Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace bit
{
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps, int n);
void STDestroy(ST* ps);
void STPush(ST* ps, STDataType x);
void STPop(ST* ps);
STDataType STTop(ST* ps);
int STSize(ST* ps);
bool STEmpty(ST* ps);
}
// Stack.cpp
#include"Stack.h"
namespace bit
{
void STInit(ST* ps, int n)
{
assert(ps);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
void STPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
printf("扩容\n");
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
}
// Queue.h
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace bit
{
typedef int QDataType;
typedef struct QueueNode
{
int val;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
}
// Queue.cpp
#include"Queue.h"
namespace bit
{
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
}
// test.cpp
#include"Queue.h"
#include"Stack.h"
typedef struct Stack
{
int a;
int top;
}ST;
void STInit(ST* ps){}
void STPush(ST* ps, int x){}
int main()
{
ST st1;
STInit(&st1);
STPush(&st1, 1);
STPush(&st1, 2);
printf("%d\n", sizeof(st1));
bit::ST st2;
printf("%d\n", sizeof(st2));
bit::STInit(&st2);
bit::STPush(&st2, 1);
bit::STPush(&st2, 2);
return 0;
}
这里我就区分了全局变量和域里面的,全局变量的可以直接用,域里面的要用域的表达式,并且我们看见了,不同文件,若域相同,多个文件可以只创建一个命名空间,会认为是一个命名空间。就是这个意思。
6.4命名空间展开
每次都指定命名空间::实在有些麻烦,于是我们引入了using关键字。但是如果展开了和C语言没有啥区别了,这样他的优势就没有了。
关于命名空间开展。我这里为大家来做跟具体的讲解,C++也是先局部在全局的找一个变量的名字,如果找到了,那他就停止找,如果没有找到,他就继续往下一层找,还记得我上次说的,为什么库函数为什么和全局变量rand矛盾吗,就是因为他们都是同一个域,也就是全局域,里面有二个名字,编译器不知道是哪一个,这里我再强化一下,命名空间的展开,他也是和全局变量同一个级别的,帮我们展开后,如果也出现和命名空间一样的名字,我们也会冲突,所以这样写就破坏了命名空间的设计的初中,但是日常写代码应该不会有冲突的,所以我们可以展开。
例1 全局 局部问题
cpp
int g_val = 10; // 全局变量
using namespace ns; // ns里也有int g_val = 20;
void test() {
int g_val = 30; // 局部变量
cout << g_val; // 输出30,找到局部就停止,不会找全局/ns里的
}
这里虽然全局都有g_val变量但是他不会冲突,因为先局部找,找到停止。
例二 C++ 全局 + using namespace 直接冲突 最简例子(能直接编译报错)
cpp
#include <iostream>
// 1. 全局作用域 变量
int rand = 100;
// 2. 引入std整个命名空间,std里本身就有 std::rand 函数
using namespace std;
int main()
{
// 这里直接报错:二义性
// 编译器分不清 全局rand 还是 std::rand
cout << rand << endl;
return 0;
}
为什么冲突?
-
你自己定义了全局变量 rand
-
using namespace std; 把 std::rand (库函数) 拉到了全局可见范围
-
两个 rand 同级别,编译器分不清你要用哪一个 → 直接报歧义错误
6.4.1全展开与部分展开
cpp
// 全展开
using namespace bit;
// 部分展开
using bit::rand;
展开整个命名空间的风险很大但是方便,平时的小练习和个人项目是可以这么整的。
折中的方式:把某个成员展开------展开常用并且不存在冲突的成员。
6.4.2区分展开头文件和展开命名空间
展开命名空间和展开头文件不是一回事。
-
展开头文件是在预处理的时候在该
include<>处拷贝该头文件的内容。 -
展开命名空间是指将该被展开变量从"默认去全局域查找"变成"到指定命名空间域中去查找"。
6.4.3输入输出命名空间的展开详解
这里我还没有讲到输入输出,现在这里为大家讲一下,方便大家理解。
C++ 头文件 + 命名空间 std 全套总结
1. 头文件是什么
2. 头文件( .h / .hpp /系统头文件如 )只是一个文件,本身不是作用域、不是命名空间。
3. #include 本质就是:把头文件里的所有代码,原封不动复制粘贴到你的 cpp 文件里.
4. 头文件里只做声明(告诉编译器有这个东西),不做具体实现;真正的函数/功能实现,在系统库里,靠链接绑定。
5. cout、cin 到底在哪
6. 头文件内部,自带写了一个 namespace std 域。
7. cout 、 cin 、 endl 全部声明在 std 这个命名空间里面,天生就被包在 std 域里。
8. 所以不用 using namespace std; 时,必须写 std::cout 、 std::cin 才能用。
9. using namespace std; 作用
- 把 std 域里所有名字 全部拉出来,放到全局可见范围。
- 之后可以直接写 cout 、 cin ,不用加 std:: 。
- 缺点:和全局自己定义的同名变量/函数会同级别冲突,引发二义性报错。
13. 是不是所有库都在 std 里?
- 不是所有东西都在 std 里。
- 现在学的: cout、cin、string、vector、stack、queue 这些在 std 主域。
- C 语言老头文件: #include <stdio.h> 、 #include <stdlib.h> 里的 printf、scanf、rand 不在 std,直接在全局域。
- C++ 还有很多子域/其他命名空间(比如 std::chrono 、 std::filesystem ),不是全部挤在一个 std 里。
18. 域的层级关系
- 有 全局域、自定义命名空间域、std 标准库域、函数内部局部域。
- using namespace 域; 会把该域名字提升到和全局同级别,同名就冲突。
- 局部域优先级最高,会屏蔽全局和命名空间的同名变量。
22. 一句话终极口诀
- 头文件只是复制代码,本身不是域;
- cout/cin 天生藏在 std 域里;
- 不是所有库都在 std,C 老头文件在全局;
- using 展开域会和全局同名打架。
6.5命名空间细节补充
- 标准命名空间std:是C++标准库中所有类、对象、函数和模板等元素所在的顶级命名空间。它将标准库的功能与用户自定义的代码隔离开来,防止名称冲突。
- 局部域和全局域会影响生命周期,命名空间域和类域不会影响。
- 只能在全局定义命名空间,不可以在局部定义命名空间。
- 一个域能定义很多东西。
- 命名空间域中的变量生命周期仍然是全局的,它本质上还是全局变量。只是在查找的时候受到了限制。
- 命名空间不是结构体! 后面没有分号。
using展开语句后面有分号。 - 命名空间实际上可以反复无限嵌套的,但是不建议这么用。
七、输入和输出
7.1解决C语言的问题:不支持自定义类型的输入输出
头文件:iostream(IO流)
- 是 Input Output Stream 的缩写。
- 类似于C语言的stdio.h,是标准的输入、输出流库,定义了标准的输入、输出对象。
- C++最老的版本,头文件是加了.h的。只有祖宗级别的编译器上才能看到(如VC6.0)。
- stdio.h和iostream的关系非常微妙,部分编译器上一脉相承,iostream包含了stdio.h,而某些编译器上并不支持。
7.2输出流和流插入
7.2.1输出流介绍
std::cout是输出流的一种,它可以把数据输出到控制台/终端。
除了cout,还有很多输出流,方便我们输出数据到文件/数据库等处。
7.2.2输出原理
首先,cout的c是character的意思。在内存中,存储有整型、浮点数等类型的区分,但是在其他地方比如说------控制台,它没有。即,数据在输出时一律会被转换成字符再输出。cout输出到控制台正是将内存中的东西以字符的形式输出到控制台中。
7.2.3使用方式
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 10;
double b = 3.14;
char c = 'x';
cout << a << " " << b << " " << c << endl;
return 0;
}
7.2.4流插入的特性
C++输入输出更方便,不需要像C语言的printf/scanf那样需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的)。
cpp
int a = 0;
double b = 0.1;
char c = 'x';
cout << a << " " << b << " " << c << endl; // 方式一
std::cout << a << " " << b << " " << c << std::endl; // 方式二
7.2.5空格和换行
插入" "表示空格,插入endl表示换行。(不要忘记endl被定义在std命名空间里面)
换行的另外几种方法(基于C++兼容C的小巧思):
cpp
cout << a << " " << b << " " << c << "\n" << '\n' << endl;
7.3输入流和流提取
输入流和流提取可以概括为一句话:
cin自动从控制台识别类型并提取数据 ,>>变量,顺序是从cin由近到远。
cpp
int a;
double b;
char c;
cin >> a >> b >> c;
注意 :cin后面不能用endl!
7.4输入输出细节补充
-
关于IO流体系:IO流涉及类和对象、运算符重载、继承等很多面向对象的知识,这里我们只能简单认识一下C++ IO流的用法,后面会专门出文章来细节IO流库。
-
使用注意 :
cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用他们。 -
警告 :一般日常练习中我们可以
using namespace std,实际项目开发中不建议using namespace std。 -
精度控制小巧思:C++可以控制小数点,但是有点麻烦,需要精度控制函数:
cpp
double a = 2.2222222222;
cout << cout.precision(2) << a << endl; // 控制两位小数
但是C++兼容C,所以可以使用C语言的那一套更加方便的方法:
cpp
double a = 2.2222222222;
printf("%.2lf", a);
- 提升效率小技巧:C++为了兼容C语言,在一些方面需要付出一些代价。如缓冲区绑定,缓冲区多次刷新,影响性能。在高IO需求的情况下可能不通过。
解决办法一:直接使用printf和scanf。
解决办法二:加入以下三行代码,具体什么原理以后会讲:
cpp
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
7.4.1关于C++缓冲区的讲解
这里我们先要知道cout里面的数据实在缓冲区的,只有刷新一下缓冲区我们才会有写入的显示,上面我们讲了
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
这三个我大致说一下,他们的作用。
我来给你逐行解释这三行代码的作用,以及它们的关系👇
7.4.1.1代码 1ios_base::sync_with_stdio(false);
作用:关闭C++流与C语言stdio的同步
- 默认情况下, cin / cout 和 scanf / printf 会共享缓冲区、互相同步,为了兼容C语言
- 关闭同步后,C++流就不再和C语言流互相等待、同步,减少了大量锁、检查、同步开销
- 效果:cin/cout 速度大幅提升,甚至超过 scanf/printf
- 注意:关闭后,不能再混用 cout 和 printf ,否则会出现输出乱序
7.4.1.2代码2: cin.tie(nullptr);
作用:解除 cin 与 cout 的绑定
- 默认情况下, cin 会绑定 cout :每次执行 cin >> 时,会先自动刷新 cout 缓冲区
- 解绑后, cin 不再强制刷新 cout ,消除了每一次输入前的多余刷新操作
- 效果:进一步减少无用开销,速度再提升一档
- 代价:解绑后, cin 不会再帮你自动刷新 cout ,输出内容必须用 \n 或 flush() 手动刷新
7.4.1.3代码3: cout.tie(nullptr);
作用:解除 cout 与 cin 的反向绑定(补充操作)
- 标准规范里, cout 默认也会绑定 cin ,不过在大部分编译器中, cin.tie(nullptr) 会自动解除 cout 到 cin 的绑定
- 这行是"双保险",确保 cout 也不会因为等待 cin 而产生额外开销
- 效果:和第二行几乎一致,是一个更彻底的解绑写法,速度不会再提升,但写法更严谨
总结:三行代码的整体效果
代码 作用 效果
sync_with_stdio(false)关闭C/C++流同步 消除跨语言同步开销,提速
cin.tie(nullptr) 解除cin与cout的绑定 消除cin输入时的自动刷新开销,再提速
cout.tie(nullptr) 解除cout与cin的绑定 补充双保险,彻底消除双向绑定的开销 .
一句话理解
这三行代码,就是为了把cin/cout的性能压榨到极致,通过关闭同步、解除绑定,消除所有多余的检查和刷新操作,让C++流的速度达到最高,专门用来应对大数据输入输出的场景(比如算法竞赛、OJ刷题)。
补充说明
日常写作业时,只写前两行就足够了,第三行可以省略;只有在追求极致性能或者规范写法时,才会加上第三行。
7.4.1.4变快原因以及缓冲区怎么刷新的
缓冲区 & 解绑刷新:完整逻辑总结
一、核心前提:默认状态下,C++流有两个"拖慢性能"的绑定
- ios_base::sync_with_stdio(true) :默认开启,C++流( cin/cout )和C语言流( scanf/printf )共享缓冲区、强制同步
- cin.tie(&cout) :默认绑定,每次执行 cin >> 时,必须先刷新 cout 缓冲区
二、为什么解绑后速度会变快?(逐点拆解)
- 解绑 sync_with_stdio(false) :消除跨语言同步开销
- 解绑前:C++流和C语言流互相兼容,每次读写都要做缓冲区同步、加锁、等待队列检查,大量冗余操作拖慢速度
- 解绑后:C++流独立运行,不再和C语言流互相迁就,直接跳过所有同步开销,速度直接暴涨
- 解绑 cin.tie(nullptr) :消除"无用刷新"的性能浪费
- 解绑前:只要 cin 和 cout 绑在一起,每执行一次 cin >> ,不管你有没有输出内容,都会强制刷新 cout 缓冲区
- 日常写作业: cout 输出提示后 cin 输入,刷新是必要的,不会浪费
- 刷题/大数据场景:全程只用 cin 读数据,根本没输出, cin 依然会白白刷新空缓冲区,十万次循环就是十万次无用操作,直接拖慢超时。这里就是我要解绑cin和cout原因,这里我用代码为大家解读一下。
cpp
#include <iostream>
using namespace std;
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr); // 彻底解绑
int x;
for(int i = 1; i <= 100000; i++)
{
cin >> x;
// cin 和 cout 已经解绑
// 不会没事强行刷新cout
// 该干嘛干嘛,没有多余操作 → 更快
}
return 0;
}
这里就是cin不断刷新缓冲区,造成多余操作,导致变慢。
- 解绑后: cin 不再管 cout ,只在你主动输出时才会刷新,消除了所有不必要的自动刷新,速度再上一个台阶
- cout.tie(nullptr) :双向解绑的"双保险"
- 标准规范中, cout 也默认绑定 cin ,解绑这一行可以彻底消除反向绑定的开销,属于严谨写法,进一步确保无多余操作
三、解绑后,为什么还能刷新缓冲区?(刷新的三种方式,和解绑无关)
解绑只是关闭Conditions for Automatic Refresh件,并没有关闭"刷新缓冲区"这个功能,刷新依然有三种途径:
- 主动触发: \n 换行
只要输出内容末尾加 \n ,系统会自动触发缓冲区刷新,内容立刻显示(推荐用 \n 代替 endl , endl 会强制刷新,反而慢)其实这里endl刷新规则和\n不同感兴趣的同学可以自己去研究一下。 - 手动强制: cout.flush()
不想换行但要立刻显示,直接调用 cout.flush() ,强制把缓冲区内容刷到屏幕 - 兜底触发:程序正常结束
只要程序正常运行退出,系统会强制把缓冲区里所有没输出的内容全部刷新打印,不管有没有换行/flush
一句话总结
解绑的本质是消除"强制同步"和"无用自动刷新"的性能浪费,让C++流只在你需要的时候才刷新缓冲区,从而大幅提速;刷新缓冲区的功能本身并没有消失,依然可以通过 \n 、 flush() 或程序结束来触发。
所以把C++和C语言解绑,并且把cout和cin解绑这样效率最快。
八、缺省参数
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
8.1样例解析
cpp
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值0
Func(10); // 传参时,使用指定的实参10
return 0;
}
在形参的后面加上=0默认值,在调用这个函数的时候,如果我们没有传递任何参数,那么采用这个缺省参数。否则使用传递的参数。
8.2全缺省和半缺省
全缺省 就是全部形参给缺省值,半缺省就是部分形参给缺省值。
cpp
// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
// 半缺省
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
8.3缺省参数逻辑和调用给参逻辑
8.3.1核心逻辑
设参逻辑:
- 默认值必须从右边开始连续地给。
- 比如:
func(a, b, c=3, d=4)或func(a, b=2, c=3, d=4)是对的。 func(a=1, b, c=3, d=4)是错的,因为跳过了 b。
调用逻辑:
- 传参数的时候,从左边开始一个个给。
- 比如:
func(1, 2)就是给 a 和 b 赋值,c 和 d 用默认值。 - 你想给 d 赋值,就必须先把 a, b, c 都给了(或者 a, b 用默认值),不能跳着给。
总结一句话:设置默认值是"右边连续",调用传参是"左边开始"。
8.3.2应用场景
以初始化栈为例:
cpp
// 声明
void STinit(ST* st, int n = 100);
// 调用情况一:不知道要开辟多少空间
STinit(&st); // 直接给一个缺省参数,不用去管n传递多少
// 调用情况二:已经知道要插入1000个数据
STinit(&st, 1000); // 直接传递n为1000,避免反复开辟空间
两者配合起来------达到了"不知道就不传递,知道就传递"的效果。
这里其实也是为了效率,如果我们不是缺省参数的话,那我们就要开辟空间,这样不仅造成内存浪费,还会要每次都操作很不好,所以我们在初始的情况上传个缺省参数是非常好的操作。
这里很多同学误解这个用法,认为这个可以为我们开辟空间,以后想用多大再改初始的参数就好了,这就错了,这个方法适用于我们知道我们要多少的数据大约,不能再扩容上面直接改初始的参数,下面就有一个例子说明
cpp
// 错误写法:二次调用STinit
void STinit(ST* st, int n = 100) {
st->a = (int*)malloc(sizeof(int) * n);
st->top = 0;
st->capacity = n;
}
// 当空间不够时,想这样改
STinit(&st, 200); // 这会出大问题!
问题在哪?
-
新的 malloc 会申请一块新的200个空间
-
旧的 st->a 指针被覆盖,之前存的数据全部丢失
-
旧内存没有被释放,造成内存泄漏
所以,绝对不能通过"重新调用初始化"来扩容。
总结
-
初始化时的缺省参数,只是一次性开辟初始空间
-
这个初始值一旦定了,就不能再通过"改初始参数"来扩容
-
扩容只能靠 realloc 这种方式,不能回头改初始化的参数
这里再补充一个定义关于声明和和定义与缺省参数之间的关系。
函数声明和定义分离时,缺省参数只能在声明里写,不能两边都写。
比如正确写法:
cpp
// 声明(.h 里)
void STinit(ST* st, int n = 100);
// 定义(.cpp 里)
void STinit(ST* st, int n) { // 这里不能再写 =100
// 初始化逻辑
}
这是为了避免声明和定义里的缺省值不一致,导致歧义。
8.4缺省参数的细节
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
原因:声明和定义都给了,如果不一样会存在歧义。
九、函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。
C语言是不支持同一作用域中出现同名函数的。
9.1类型一:形参个数不同
cpp
// 形参个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
int main()
{
f(); // 调用f()
f(10); // 调用f(int a)
return 0;
}
9.2类型二:形参类型不同
cpp
// 形参类型不同
int Add(int left, int right)
{
cout << "int Add(int left, int right)" << endl;
return left + right;
}
double Add(double left, double right)
{
cout << "double Add(double left, double right)" << endl;
return left + right;
}
int main()
{
Add(1, 2); // 调用int版本
Add(1.5, 2.5); // 调用double版本
return 0;
}
9.3类型三:形参顺序不同
本质上是形参的类型不同。
cpp
// 形参类型顺序不同
void f(int a, char b)
{
cout << "f(int a, char b)" << endl;
}
void f(char b, int a)
{
cout << "f(char b, int a)" << endl;
}
int main()
{
f(1, 'x'); // 调用f(int, char)
f('x', 1); // 调用f(char, int)
return 0;
}
9.4特别注意:不建议缺省参数和重载共同使用
cpp
// 下面两个函数构成重载
// f()但是调用时,会报错,存在歧义,编译器不知道调用谁
void f1()
{
cout << "f()" << endl;
}
void f1(int a = 10)
{
cout << "f(int a)" << endl;
}
int main()
{
f1(); // 歧义!可能调用上面两个中的任何一个
return 0;
}
原理 :如果这个时候去调用f1()函数,没有传递参数,调用下面这个,存在歧义。不要这么写。
如果坚决要这么做,只能将他们放在不同的命名空间里面,但这就不是函数重载了,因为函数重载需要在同一个域中实现。
函数重载的最后一个小问题:返回值不同不构成重载------关键是调用歧义的问题。
总结
本篇文章我们从以下几个方面系统学习了C++入门知识:
-
C++发展历史:从1979年起源到1998年标准化,再到如今的C++23
-
应用领域:服务器端、游戏、嵌入式、机器学习等8大领域
-
学习方法:勤动手、多总结、多做笔记
-
第一个程序:C语言版和C++版两种实现方式对比
-
命名空间:避免命名冲突的核心语法
-
输入输出:cin/cout的基本使用
-
缺省参数:函数参数的默认值设置
-
函数重载:同一作用域中同名函数的多种形态
后续还会继续学习C++的更多知识,期待与大家一起深入探索C++的魅力!
标签:C++、编程入门、软件开发
感谢你的收看,如果内容对你有帮助,不要忘了三联支持哦!🚀