文章目录
c++入门学习概念
今天这篇文章开始正式开启c++的学习,本篇文章的重点是介绍一些c++内的基础概念,方便后续理解新的知识。同时与c语言进行一些对比
c++的第一个程序
说到编程语言的第一个程序,那必然是能够成功的输出HelloWorld了,现在让我们来看看如何在c++文件中输出这个语句:
cpp
#include<iostream>
int main() {
std::cout << "Hello World!" << std::endl;
return 0;
}
当然有些会稍微改进一下:
cpp
#include<iostream>
using namespace std;
int main() {
cout << "Hello World!" << endl;
return 0;
}
这就跟第一次学习c语言的时候一样,基本上是看不明白为什么这么写的,但是这个部分只是引入一下。后面将会对这些概念做讲解,以便能较快入门c++。
在这里我们先记住,#include<iostream>
这个操作就和#include<stdio.h>
一样重要即可。
命名空间
namespace关键字
在c语言程序中我们常常会碰见一下一个情况
c
#include<stdio.h>
#include<stdlib.h>
int rand = 0;
int main(){
printf("%d", rand);
return 0;
}
进行编译会发现这是会报错的。因为我们知道在stdlib.h头文件中包含了库函数rand,但是我们又定义了一个全局变量rand,那这就导致命名冲突了。这是c语言的一个弊端。那有没有办法能区分呢?
答案就是使用关键字namespace进行操作,进行一个命名空间的声名。在该命名空间内,变量声名是不能重复的。但是当前命名空间内与外界(全局),库函数中的变量却是可以同名的
我们先来看看namespace的解释:
1.定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
2.namespace本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不在冲突了。
3.C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。namespace只能定义在全局,当然他还可以嵌套定义。
4.项目工程中多文件中定义的同名namespace会认为是⼀个namespace,不会冲突。
5.C++标准库都放在⼀个叫std(standard)的命名空间中。
现在来一条一条解释:
第一、二条
也就是说,定义一个命名空间的格式是 namespace 空间名{},{}内就是在这个空间内的成员,成员不仅仅可以是变量,也可以是函数、类型(类)等。
现在来看一段代码:
cpp
#include<iostream>
#include<stdlib.h>
int rand = 0;
namespace hh{
int rand = 0;
int a = 0;
int add(int x,int y){
return x + y;
}
}
int a = 0;
int main(){
...
}
现在我们来看,全局区定义了一个叫"hh"的命名空间,是一个独立的域。里面定义了一个变量叫rand、还有一个变量a、函数add。全局区域定义了一个变量a,stdlib.h文件中还有函数rand。有很多名字是冲突的。但是经过命名空间的处理,这个问题就被解决了。也就是说,这个域是隔离其他之外的一个单独的空间,内部的名字不冲突即可。
当然可以定义多个不同的命名空间,每个空间只要保证内部不冲突就行,即使每个空间内声名的变量名以及函数名一样也不怕。
第三、四条
C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑。
局部域就可以理解为在一个函数内部开辟一个临时变量,当这个函数结束调用,函数栈帧会被销毁,那么这个临时变量就会被销毁,那么其生命周期只存在于这个函数调用期间。
而全局域即全局变量,从程序进程开始就存在于系统中,直到函数进程结束才销毁。其生命周期和程序进程有关。而因为命名空间只能在全局区域定义,所以类似于全局变量,并没有改变其生命周期,只不过是使用的时候要额外使用一些查找逻辑。
而类是c++的另外一个知识点,不是入门知识点,在这篇文章中先不进行讲解。
如果我们使用多文件管理,特别是在项目开发过程中难免会碰到不同程序员之间的命名也会冲突,这个时候就可以进行嵌套定义,隔离开两个域,这样子就不会起冲突了
cpp
#include<iostream>
namespace GDUT {
namespace yang {
int a = 10;
int b = 1;
}
namespace liu {
int a = 20;
int b = 2;
}
namespace chen {
int a = 30;
int b = 3;
}
}
也就是将GDUT这个域分为三部分,这三部分又是相互独立的,那么内部的成员变量名字不同也是不会出问题的。
当然在多文件管理中可能也会出现这么个情况,那么我们也可以使用命名空间操作对函数的定义和函数的实现加入到自定义的域中,防止出现命名冲突。但是多个文件下,如果起太多不相同的域名是很难记住的,所以c++规定了不同文件中只要命名空间名是一样的,那么系统将会把苏所有的同名的认为是同一个命名空间域。
第五条
c++标准库内有标准的命名空间std(standard),我们在第一个部分的时候就用到了,这个等下说到c++的输入输出的时候再说。
总的来看,c++解决了c语言中会出现的命名冲突问题,这是非常大的改进。
域作用限定符
对命名空间概念讲完后,这个时候就得想,那该如何使用不同域中的同名变量呢?
这个时候就需要用到域作用限定符::。在之前学习c语言的时候,我们遇见过下面这个案例:
cpp
#include<stdio.h>
int a = 1;
int main(){
int a = 2;
printf("%d", a);
return 0;
}
全局变量有a,局部变量也有a,c和c++都是考虑就近原则。优先访问近的那个。
cpp
#include<iostream>
#include<stdlib.h>
int rand = 0;
namespace hh{
int rand = 0;
int a = 0;
int add(int x,int y){
return x + y;
}
}
int a = 0;
对于这种情况,该如何访问呢?
现在来介绍一下如何使用域作用限定符::,先看代码:
cpp
int main(){
printf("%p", rand);//访问库函数rand地址
printf("%d", hh::rand);//访问hh中变量rand
printf("%d", ::a);//访问全局变量a
printf("%d", hh::a);//访问hh中变量a
printf("%d", hh::add(1, 2));//访问hh中成员函数add
return 0;
}
以上展示的非常清楚,就是空间名::成员,当出现多层嵌套的时候,如刚刚举得例子,则需要多使用一次这个作用符GDUT::yang::a,这样就可以成功访问了。
当符号前面什么也不加的时候就是访问全局变量。
展开命名空间
如果每次都这样子去使用域作用限定符是很麻烦的,那有没有办法能解决呢?
答案就是展开命名空间,使用关键字using。这样子就不需要加入那么麻烦的作用符号了。如一开始写的程序就使用了展开命名空间:using namespace std;
,即把标准命名空间展开,这样子就很方便。
当然还可以对某个成员进行展开,如using hh::a;
,即把a展开,那么默认就是使用这个。
但是这个方法还是有不小的弊端的。如果是平时自己写一些代码练习就不怕,如果是写一些较大的项目,一旦展开就很容易发生命名冲突。平时练习因为需要多次使用std标准库内的函数,所以进行展开是非常方便的。但是做项目的时候不推荐。
c++的输入输出
接下来我们简单讲一下c++中的输入和输出。因为底层实现比较复杂,以当前知识很难理解,但是在后续的学习中需要使用,所以就直接介绍用法。
流插入/提取运算符
在第一个部分介绍输入输出的时候,发现用到了两个很熟悉的运算符<<和>>,前者叫流插入运算符,后者是流提取运算符。
这两个符号于c语言中的左移右移操作符是一样的,但是不同地方的使用表达的意思不同。
在之前文件学习中就介绍过,c语言其实是通过流进行各种输入输出操作的。当前知识无法解释底层如何做的,就先记忆即可。
输入/输出
上面提到,c++中的<iostream>
文件和c中的<stdio.h>
作用是一样的,一样重要。
<iostream>
是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。包含这个文件也是可以直接使用scanf
和printf
的。
std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。
std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流。
std::endl 是⼀个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
里面的很多概念当前理解是很困难的,就简单说一下:
cin、cout、endl都是包含在std标准命名空间中的成员,使用的时候需要使用域作用限定符或者展开std这个命名空间。
cout我们当前可以简单理解为标准输出流,即控制台。当然这个输出端也可以是文件流、网络、数据库等。只不过当前包含的是标准输入、输出流的库,所以这个会输出到控制台上。
endl其实是一个函数,使用函数重载实现的,当前简单的认为是换行符并且刷新缓冲区即可
语句:cout << "Hello World" << endl;
就是将字符串"Hello World"和换行符按顺序、以字符流的形式插入标准输出流cout。
c++的输出最方便的就是不用像c语言一样考虑输出数据的类型而使用占位符。c++会自动识别输出的内容的数据类型,并且以字符流的形式输出。
cpp
#include<iostream>
using namespace std;//std展开 自己写点小代码比较方便
int main() {
int i = 10;
double j = 10.5;
char s[] = "abd";
cout << i << " " << j << " " << s << endl;
return 0;
}
输出结果:
这是非常方便的。而且可以根据想要输出的内容连续输出多个变量或者字符串。
输入操作则是使用cin,cin的作用当前可以理解为从标准输入流提取字符流存储到指定位置:
cpp
#include<iostream>
using namespace std;//std展开 自己写点小代码比较方便
int main() {
cout << "请输入一个整数" << endl;
int a = 0;
cin >> a;//从控制台提取输入到a
cout << a << endl;
//也可以提取很多个
int b = 0;
double c = 1.0;
cout << "请输入两个整数 一个浮点数" << endl;
cin >> a >> b >> c;
cout << a << " " << b << " " << c << endl;
return 0;
}
结果就不进行展示了,当前只需要了解如何使用即可。
IO效率提升
在简单提及一下 因为cout和cin的效率其实会比scanf和printf差一些
在高IO需求的地方 如竞赛、刷题网站,可以加入一下三条代码,提升io的效率
cpp
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
缺省参数
现在再来介绍下一个概念------缺省参数,也是默认值的意思。
缺省参数在定义函数的时候经常使用到,会十分方便。
来看解释:
1.缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
2.全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须右往左依次连续缺省,不能间隔跳跃给缺省值。
3.带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
4.函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明缺省值。
全缺省参数和半缺省参数
1.全缺省参数
cpp
void Func(int a = 10, int b = 20, int c = 30)
{
cout << a + b + c << endl;
}
发现相比于以往的函数定义,给参数加了一个默认值。
按照规则调用一下这个函数:
发现当参数的部分,就会默认使用缺省值。传参数的部分,就使用传入的参数。
注意,参数一定不能跳跃的传,如:Func(1, ,3)
。这是规定。
2.半缺省参数
有全缺省,就会有半缺省。注意,缺省参数不能跳着给,必须从左到右连续。
参数不能跳着传,上面说到的。同时,没有给缺省参数的位置一定要传参:
总结就是,给定缺省参数必须连续,无缺省值的也要连续。
缺省参数声名注意事项
这一点是对带有缺省参数的函数的定义和声名进行区分。当定义和声名分离时,只能在定义给出缺省值。否则会出现下面这个问题。
其实蛮好理解,因为一旦定义给出缺省值了,那么声名的时候再给,可能会给不同的值,那么调用的时候就会识别不到,调用有歧义。
缺省参数的距离应用
缺省参数是十分方便的,就比如以往实现的栈。初始化的时候都是将空间置为0。然后倍增扩容。使用realloc频繁扩容是非常影响效率的。但在c++中使用缺省参数就方便得多了。
cpp
void StackInit(Stack& st, int n = 4) {
st.top = 0;
STDataType* New = (STDataType*)malloc(sizeof(STDataType) * n);
if (!New) exit(-1);
st.capacity = n;
st.a = New;
}
这里用到了引用,等下会讲,现在就先按照以往扩容的方式理解就可以。
如果知道一开始要插入的数据有多少个,一把将空间开辟完成效率就高多了。如果不知道也没关系,有缺省值。当不传入开辟空间个数的时候,默认按照缺省值n来初始化即可。这非常的方便,会大大地提高效率。
函数重载
C++支持在同⼀作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态行为,使用更灵活。C语言是不支持同⼀作用域中出现同名函数的。
函数重载的情况
形参不同分为三种情况:顺序不同、类型不同、个数不同:
cpp
// 1、参数类型不同
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;
}
// 2、参数个数不同
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同
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;
}
这也是非常方便的。
假设想要写一个加法函数,且能够针对不同数据类型给出返回值,在c语言中是很麻烦的。因为c语言中不支持同一个作用域内函数名能一样。而c++可以,c++会根据参数的类型自动识别应该传入哪个函数进行调用。也就是上面代码的第一个部分。这样子仅仅只需要使用add这个名字就可以实现各种种类数据的相加了,这是非常方便的。
注意事项
1.构成函数重载的条件一定是参数不同
即上面的三种参数不同情况。如果只有返回值不同是无法构成的。
如代码所示:
cpp
void f()
{
cout << "f()" << endl;
}
int f()
{
return 10;
}
函数同名,但参数相同。调用的时候即f()
,编译器此时没办法根据参数不同而识别应该调用哪个函数。对于返回类型是无法识别的。此时就出现了函数的调用歧义。
2.构成函数重载也不一定能正常使用
这种情况出现在带有全缺省参数的函数和无参函数的重载

发现出现了对重载函数调用不明确的错误:
这两个函数一定是构成函数重载的,因为一个有缺省参数,一个无参,很明显的参数不同。
当传入参数的时候,很明显只有下面那个函数能够接收参数,所以调用明确。但是不传入参数的时候,由于无参不能传入参数、而带有缺省参数的也可以不传参。那此时编译器就不知道应该调用哪个函数了,所以会报出这样的错误。这一点是非常需要注意的!
引用概念和定义
引用这个概念,就相当于是给一个变量取别名。
引用变量的简单使用
引用的声名:类型& 引用别名 = 引用对象;
注意:引用不是变量,是取别名!

如图代码所示:b、c给a取别名,d给b取别名,那最后其实还是给a取别名。也就是说,一个变量可以有多个引用。
那最后b、c、d其实都是a的别名,即四个其实是一样的值。引用不是指针。指针是开辟一个空间存储变量的地址:
然后就可以通过这个指针变量找到a的位置并对其解引用,这是间接的操作。
而引用是取别名,不会开辟新的空间,别名和原来的名字都是同一个位置的:
即abcd本质上是同一个值,就像一个人,除了大名还会有小名、外号等。叫这些别名的时候说的都是同一个人。这里的引用也是这样的。
引用的特性
1.引用必须初始化
这个很好理解,总得有变量才能取别名。
2.引用一旦使用过后,就无法改变指向
如图所示,给a取别名b之后,让b = d这个操作不是让b重新变为d的别名,而是赋值操作。因为b是a的别名,所以改变b也会改变a。
引用的便捷之处
我们发现,引用虽然和指针不是一个用法,但是有些类似。引用在一定程度上是很方便的:
如我们常写的交换函数swap:
cpp
void swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
int main() {
int a = 10, b = 20;
cout << "交换前:>>>\n" << a << " " << b << endl;
swap(a, b);
cout << "交换后:>>>\n" << a << " " << b << endl;
return 0;
}
运行一下看看:
发现交换成功了。这是为什么呢?
原因就在于引用的特性,将a、b作为参数传入,swap函数内的x相当于给a取了别名,y相当于给b取了别名:
那这样来看,交换x和y不就是交换了a和b吗?
所以引用是非常便于理解的。就不用传指针修改值了。效果一样,但是却更容易理解了,代码的可读性增强了。
当然会有人说,那么以后不用指针了,全部用引用不就好了。这是不行的,因为刚刚说了,引用一旦确定就不能再改变了。如果实现一个链表,不用指针链接反而用引用,那怎么增删节点呢?引用都无法改变了。所以指针和引用其实是相辅相成的。
使用引用就可以使函数修改参数的值得时候可以不需要传址,这是非常大的进步!
const引用
权限
如现在定义一个变量const int a = 10;
也就是说,这个a是一个常变量,无法修改的。限制了对a的访问权限只能是读取而不能修改。
若现在想通过引用间接修改int& b = a; b++;
这样是会报错的
因为对a的权限只能读,使用int&引用就可以读和写,这放大了权限,这是不可行的。
权限只能平移或者缩小,不能放大。所以应该这样引用:const int& b = a;
,平移权限,限制了b的权限只能读不能修改,这样子就不会报错了。
当然可以缩小权限:
cpp
int a = 10;
const int& b = a;
这样子是完全没有问题的,只是对于b这个别名来讲,限制了其的权限。
这个很好理解,就像一个名人,在自己家里可以随心所欲地干一些事情,但是到了公共场合就得注意自己的言行举止。这不就是缩小了权限吗。
对临时变量(常量)的引用
这个部分其实和权限也是有关系。
类似 int& rb = a*3; double d = 12.34; int& rd = d;
这样一些场景下a*3的结果会保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值,也就是时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,只能读不能修改,所以引发了权限放大问题。所以仍然是需要进行const修饰。
cpp
const int& rb = a * 3;
const int& rd = d;
过程很好理解:
至于类型强转的图解就不画了,原理类似。
引用作函数返回值
这点简单说说,这个操作是有很方便的:
cpp
//引用还能当作函数的返回值 只要这个返回值不是函数内部开辟的临时变量,就可以对其修改
//如栈的使用中
#include<iostream>
#include<assert.h>
#include<stdlib.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 栈顶
int capacity; // 容量
}Stack;
void StackInit(Stack& st, int n = 4) {
st.top = 0;
STDataType* New = (STDataType*)malloc(sizeof(STDataType) * n);
if (!New) exit(-1);
st.capacity = n;
st.a = New;
}
void StackPush(Stack& ps, STDataType data) {
if (ps.capacity == ps.top) {
//需要扩容
int tmpsize = ps.capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps.a, tmpsize * sizeof(STDataType));
if (!tmp) {
perror("realloc");
exit(-1);
}
ps.a = tmp;
ps.capacity = tmpsize;
}
ps.a[ps.top] = data;
ps.top++;
}
//正常是这么修改的
void StackModify(Stack& st,int x) {
if (st.top == 0) return;
st.a[st.top - 1] = x;
}
在以往c语言的操作中,修改栈顶元素会专门写个函数StackModify
,然后又需要传栈的地址和修改的值。
但是使用引用作返回值就方便的多:
cpp
//如果拿引用做返回值就很方便了
STDataType& StackTop(Stack& st) {
assert(st.top > 0);
return st.a[st.top - 1];
}
测试结果:
就是因为将栈顶的元素的别名返回,那也就是说StackTop(ST)就是栈顶元素别名,那直接对这个修改不就好了。注意这里的栈顶元素是开辟在堆区上的,不是临时变量,所以不加const,也不用担心函数调用结束后会销毁。
这里只是简单举个例子看看,后续将会深入学习。
引用和指针的区别
语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
引用在初始化时引用⼀个对象后,就不能再引用其他对象;指针可以在不断地改变指向对象。
引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
sizeof中含义不同,引⽤结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全⼀些。
inline内联函数
1.⽤inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,直接将所有语句展开进行执行(也就是汇编过程中没有call指令 ),就可以提高效率。
2.inline对于编译器而言只是⼀个建议,也就是说,加了inline编译器也可以选择在调用的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。
3.inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。也就是说编译器会自动识别是否需要展开
4.C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。
5.vs编译器 debug版本下面默认是不展开inline的,这样方便调试。
6.inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
nullptr空指针
NULL实际是⼀个宏,在传统的C头⽂件(stddef.h)中,可以看到如下代码:
cpp
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
1.C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷f((void*)NULL);调用会报错。
2.C++11中引⼊nullptr,nullptr是一个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
最主要的原因是c++的类型检查非常严格,void*无法让其余类型指针接收,而在c语言中是可以的。所以对此进行了改进,空指针专门使用nullptr。
所以以后使用空指针就直接使用nullptr就可以了。