ʕ • ᴥ • ʔ
づ♡ど
🎉 欢迎点赞支持🎉
个人主页: 励志不掉头发的内向程序员;
专栏主页: C++语言;
文章目录
前言
C++是在C语言的基础上所诞生的一门语言,它在C语言的基础上增加了很多的新语法、STL库以及高阶数据结构。我们在掌握C语言的基础上可以发现C++比C语言有很多方便,简洁的地方,可以大大的减少我们的劳动力,所以我们一起来学习吧。

一、第一个C++项目
C++项目前面的创建和C语言没有什么差别,唯一的差别就是在创建源文件时把后缀变成cpp(C plus plus)即可。
1.1、如何创建C++项目


如果我们的后缀是c就是调用c的编译器,如果是cpp就是调用c++的编译器。
1.2、第一个C++程序
cpp
//test.cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
这个程序想要说明C++是兼容C语言的。这样更加说明了C++是在C语言的基础上发展出来的。

当然,C++也有自己的一套输入输出,严格来说C++版本的hello world应该是这样写的。
cpp
//test.cpp
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
这里不明白也没有关系,我们接下来会依次讲解。

二、命名空间
2.1、namespace的价值
回看我们刚才的代码,我们就会看到using namespace std;中就含有namespace。
namespace是我们学习C++时接触的第一个关键字。我们在C语言的学习中或多或少就会遇到我们创建函数或者变量时忽然就报错了,原因是重定义的问题。
cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
运行时,

造成这个问题的原因也很简单,就是这个名字在同一个域中已经用过了,所以就会有歧义。但是随着语言的发展,我们的各种特性和语法越来越多,我们如何能够避免这个问题的发生呢?
这个时候我们就可以使用我们的namespace关键字了,这个关键字的作用就是我们可以把我们创建的函数、变量等,放到我们的一个新的空间域中,这样的话这个空间域就和外界隔绝开来了。这个时候我们就可以在这个空间域中随便命名了,哪怕和外界的名字一样也是可以的。此时我们如果要使用这个空间域的东西就得让计算机在这个空间域中寻找找,这样就和外界的相同名字的变量或者函数区分开来了。
这就是namespace关键字的作用和价值。
2.2、namespace的定义
我们已经知道了namespace的作用和价值了,那这个namespace具体该如何使用呢?
namespace xxx(命名空间名字)
{
命名空间成员;
变量;
函数;
........;
}
定义我们的命名空间其实很简单,在我们的namespace的关键字后面接自定义的空间名字后加一个{},在{}中写入我们要定义的命名空间的成员即可。
cpp
namespace lll
{
// 变量
int rand = 10;
}
刚才我们的代码加上这个就lll的命名空间就不会有歧义了。

重定义的报错就已经消失了,这说明我们的计算机访问的全局域中的rand而非我们的lll空间域了。这就是namespace关键字的用法。
但是我们如何去让计算机在我们创建的命名空间中寻找呢?
我们知道我们之所以有同名的类型或者函数却能够避免重定义的问题是因为我们把它们放到了不同的域中,我们的C++中一种有4种域,全局域、函数局部域、命名空间域和类域。在这些域中我们要想指定访问就得使用一个运算符
域作用限定符
::
用这个即可以使用不同的域中的类型。
cpp
#include <stdio.h>
// 命名作用域
namespace lll
{
int a = 1;
}
// 全局域
int a = 3;
// 类域(后面讲)
class llll
{
public:
int a = 1;
};
int main()
{
// 函数局部域
int a = 4;
return 0;
}
我们可以看到我们的4个不同的域中的变量a都不相同,如果我们直接输出a的话计算机就会由于就近原则而输出函数局部域的a。
cpp
printf("%d\n", a);
输出了4。

我们要是想要使用全局域就得这样,
cpp
// ::左边啥也不写,右边加上要用的变量之类的名字
printf("%d\n", ::a);
输出了3。

如果想要使用命名作用域就得这样,
cpp
//命名作用域的名字 + :: + 要使用的变量之类的名字
printf("%d\n", lll::a);
输出了1,

这就是使用不同域的方法之一。
当然,我们的域除了定义变量也可以定义别的像函数、结构等。
cpp
namespace lll
{
int a = 0;
int Add(int left, int right)
{
return left + right;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
int a = 1, b = 2;
int c = lll::Add(a, b);
// 此处要注意
struct lll::Node p1;
printf("%d, %d", lll::Add(1, 2), c);
return 0;
}
使用命名空间域中的函数和结构的方法也和变量一样。

我们的namespace只能定义在全局,不能定义在局部或者其他的地方,同时我们的namespace还可以嵌套定义。
cpp
namespace test
{
namespace zhangsan
{
int a = 10;
//......
}
namespace lisi
{
int a = 4;
//......
}
namespace wangwu
{
int a = 54839;
//......
}
}
想要使用的方法也大差不差。
cpp
int main()
{
printf("%d\n", test::zhangsan::a);
printf("%d\n", test::lisi::a);
printf("%d\n", test::wangwu::a);
return 0;
}

同时,在namespace中如果给命名空间域定义了相同的名字,编译器会认为这是同一个namespace,会把它们自动合并,所以不会冲突(在不同文件中也是一样的)。
C++标准库都放在一个叫std(standard)的命名空间中。所以我们在看C++代码的过程中会看到std::cout、std::list等等。
2.3、命名空间使用
我们要使用命名空间中定义的变量/函数有3种方式:
1、指定命名空间访问,也就是用域作用限定符,项目中推荐这种方式;
2、using将命名空间中某个成员展开,在我们经常访问的不存在冲突的成员推荐这种方式;
3、展开命名空间中的全部成员,项目不推荐,冲突概率极大,小练习时为了方便推荐使用;
第一种方法我们前面已经讲解,为了说明我们的第二和第三种方法,我们回看我们之前的第一份代码
cpp
//test.cpp
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
我们在这里就使用到了using,它这是我们的第3中用法,就是
cpp
// using namespace + 命名作用域的名字
using namespace std;
这种用法相当于就是把我们叫做std的命名空间解锁开来,把里面的东西放到全局域种来使用。这也是它为什么冲突概率极大的原因,因为相当于它没有用域来隔绝开来。
我们的第2种方法和第3种差不多,只是没有像第3种这么彻底。
cpp
namespace lll
{
int a = 10;
int b = 20;
}
// 展开部分的命名空间
using lll::a;
// 展开整个命名空间
//using namespace lll;
int main()
{
printf("%d %d", a, lll::b);
return 0;
}
这样我们就能够方便使用一些我们经常使用的且不冲突的变量/函数了。

注:展开头文件和展开命名空间都叫展开,但是前者是把内容拷贝进来,而后者则相当于把这个域的墙拆掉了。
三、C++的输入&输出
我们可以看到我们的第一个C++代码的一个新的头文件
cpp
#include <iostream>
1、<iostream>是Input Output Stream的缩写,是标准输入、输出流库,定义了标准的输入、输出对象。
2、std::cin是istream类的对象,它主要面向窄字符的标准输入流。
3、std::cout是ostream类的对象,它主要面向窄字符的标准输出流。(其实本质就是将我们输入的内容转化成字符输出的)
4、std::endl是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
5、<<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符位运算左移/右移)
6、使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的支持自定义类型对象的输入输出。
cin的作用我们现在可以简单的理解为scanf即可,cout的作用我们现在可以简单的理解为printf即可,具体是什么我们后面再来详细了解,想要用cin和cout去输入输出东西其实比scanf和printf容易的多,因为我们不需要知道每个不同类型的变量该如何输入和输出,编译器会自动识别类型。我们只需要知道cout和cin分别使用的是<<和>>即可。
cpp
#include <iostream>
using namespace std;
int main()
{
int a = 0;
double b = 0;
char c = 0;
cin >> a >> b >> c;
cout << a << " " << b << " " << c << endl;
return 0;
}
endl是换行符,相当于\n,
运行后我们发现

我们没有给cout指定类型,但是它就按照我们的一个变量一个空格的方式输出了。
当然,如果我们使用\n也依然可以达到换行的效果
cpp
cout << a << " " << b << " " << c << '\n';

我们能不能不要这样输出一个a又输出一个空格,而是把他们放到一起输出呢?肯定是不可以的,因为计算机自动识别类型,但是把他们放到一起计算机就不知道到底是什么类型了,所以我们一个<<只能识别一个类型。(cout是一个二元操作符)

四、缺省参数
1、缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
2、全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
3、带缺省参数的函数调用,C++规定必须从左往右依次给实参,不能跳跃给实参。
4、函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
cpp
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(); // 没有传参时使用参数默认值
Func(10); // 传参时使用指定的实参
return 0;
}
缺省参数就是给函数提前赋予参数(只能是常量或者全局变量),如果我们不传参就会使用其缺省参数,传参则使用我们指定的实参。

有多个参数时,如果我们每一个都赋予了缺省参数,就叫做全缺省参数。
cpp
#include <iostream>
using namespace std;
// 全缺省
int Add(int left = 1000, int right = 1000)
{
return left + right;
}
int main()
{
cout << Add() << endl;
cout << Add(1, 2) << endl;
return 0;
}

如不是,那就叫做半缺省参数。半缺省参数必须是从右往左来给予半缺省参数。
cpp
// 半缺省
void Func(int a, int b = 2, int c = 1)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
Func(10);
Func(20, 30);
Func(40, 50, 60);
return 0;
}

我们的函数给予参数时必须从左往右,不能跳跃赋参。我们的缺省参数不是从右往左的话,编译器就没有办法知道我们是像给有缺省参数的值还是没有缺省参数的值赋参了。如果半缺省参数不是连续赋参也是同理。
五、函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更加灵活。
cpp
#include <iostream>
using namespace std;
// 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;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.1, 1.2) << endl;
return 0;
}
我们可以看见我们的这两个函数同名,但是形参却不相同。

我们传了两个整型,编译器就为我们调用了整型的Add函数,而传了两个浮点型,编译器就为我们调用了浮点型的Add函数。这就是函数重载。
同时,想要达成函数重载除了类型不同还可以是个数不同。
cpp
//参数个数不同
void Func(int a, int b)
{
cout << "void Func(int a, int b)" << endl;
cout << a << " " << b << endl;
}
void Func(int a, int b, int c)
{
cout << "void Func(int a, int b, int c)" << endl;
cout << a << " " << b << " " << c << endl;
}
int main()
{
Func(1, 2);
Func(1, 2, 3);
return 0;
}

当然,也可以时参数的顺序不同。
cpp
//参数顺序不同
void Func(char a, int b)
{
cout << "void Func(char a, int b)" << endl;
cout << a << " " << b << endl;
}
void Func(int a, char b)
{
cout << "void Func(int a, char b)" << endl;
cout << a << " " << b << endl;
}
int main()
{
Func(1, 'a');
Func('x', 5);
return 0;
}

我们刚才学了缺省参数,如果我们把缺省参数也用上呢?
cpp
void f1()
{
cout << "void f1()" << endl;
}
void f1(int a = 1)
{
cout << "void f1(int a)" << endl;
}
它们是构成函数重载的,因为它们的参数个数不同,但是如果尝试着去调用它们是做不到的,因为它们会存在歧义。

不知道是调用有参的还是无参的,因为有缺省参数。
我们的返回值不同构不构成重载呢?答案其实很简单,是不构成重载的,因为编译器在调用时会面临调用歧义的问题而导致无法构成重载。
六、引用
6.1、引用的概念和定义
引用不是新定义一个变量,而是给已经存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。就比如你的名字xxx,但是叫你小名其实也是在叫你,别人给你去外号,叫你外号也是在叫你。不会因为多了个别名而多了一个你。
引用的诞生主要是由于C语言中指针使用过于复杂,所以为了避免使用指针便创造了引用。
类型 &引用别名 = 引用对象;
注:C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的<<和>>,已经现在的&,大家注意区分记忆。
cpp
int main()
{
int a = 0;
// 引用: b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
// 这里取地址我们看到是一样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;;
cout << &d << endl;
return 0;
}

6.2、引用的特性
1、引用在定义时必须初始化
2、一个变量可以有多个引用
3、引用一旦引用一个实体,不能再引用其他实体
cpp
int main()
{
int a = 10;
//ra必须初始化引用,不然就会报错
//int& ra;
int& b = a;
//这里并非让b引用c,因为C++引用不能改变指向,
//这里是一个赋值
int c = 20;
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}



6.3、引用的使用
1、引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象
2、引用传参和指针传参功能是类似的,引用传参相对更方便一点
3、引用返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入。
4、引用在实践中相辅相成,功能有重叠性,但各有特点,互相不可替代
cpp
// 引用
void Swap(int& rx, int& ry)
{
cout << "void Swap(int& rx, int& ry)" << endl;
int tmp = rx;
rx = ry;
ry = tmp;
cout << rx << " " << ry << endl;
}
// 指针
void Swap(int* rx, int* ry)
{
cout << "void Swap(int* rx, int* ry)" << endl;
int tmp = *rx;
*rx = *ry;
*ry = tmp;
cout << *rx << " " << *ry << endl;
}
int main()
{
int x1 = 1, y1 = 2;
int x2 = 32, y2 = 54;
Swap(x1, y1);
Swap(&x2, &y2);
return 0;
}
在我们之前学C语言时,我们想要用一个函数实现我们交换两个变量的效果,我们要传指针来实现,虽然也不难理解,但是看起来总是怪怪的,现在我们的实参只需要穿值,让我们的形参变成我们实参的引用就可以达到我们一样的效果,使用就更加方便了。

由于引用是给原来的变量取别名,所以我们改变了引用的值,我们原来的变量也就跟着改变了,这就是引用的传参可以改变实参的原因,也是引用的其中一个用法。
第二种用法就是传参时减少拷贝从而提高效率。在我们的函数传参时,一般是传值传参,传值传参是拷贝,假如我们把实参a传给形参x,本质上是我们把实参a拷贝一份给我们的形参x,这个时候就平白的增加了一次拷贝,但是如果我们传引用就不会有这种事情发生,因为引用编译器不会给它分配内存空间。最终实现了提高效率的结果。
6.4、const引用
1、可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大
2、需要注意的是类似int& rb = a*3;double d = 12.34;int& rd = d;这样一些场景下a*3的和的结果保存在一个临时对象中,int& rd = d也是类似,在类型转换中会产生临时对象储存中间值,所以rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用(const)才可以
3、所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象
cpp
int main()
{
const int a = 10;
//这里的引用是对a访问权限放大
//int& ra = a;
//这样才可以
const int& ra = a;
//这里在变量运算和类型转换(隐式类型转换)时会产生临时变量,临时变量具有常性
//所以这里涉及到了权限放大问题
double b = 12.34;
//int& rb = a * 3;
//int& rd = b;
//这样才可以
const int& rb = a * 3;
const int& rd = b;
//权限可以缩小,但是不能放大
int c = 20;
const int& rc = c;
//常量也是具有常性的所以得用常引用
const int& re = 20;
return 0;
}
我们可以看到,如果权限放大了编译器就会报错。

6.5、指针和引用的关系
1、语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量的地址,要开空间
2、引用在定义时必须初始化,指针建议初始化,但语法上不必须
3、引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以不断的改变指向对象
4、引用可以直接访问指向对象,指针需要解引用才是访问指向对象
5、sizeof中含义不同,引用结果为引用类型大小,但指针始终是地址空间所占字节个数
6、指针很容易出现空指针和野指针问题,引用很少出现,引用使用起来安全一点
七、inline
1、用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,就可以提高效率
2、inline对于编译器而言只是一个建议,也就是说,你加入inline编译器也可以选择在调用的地方不展开,不同的编译器在inline什么情况下展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用短小函数,对于递归函数,代码相对多一点的函数,加上inline也会被编译器忽略
3、C语言实现宏函数也会在预处理时展,但是宏函数实现很容易出错,且不方便调试,C++设计了inline目的就是替代C的宏函数
4、vs编译器debug版本下面是默认不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方
5、inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时就会报错
cpp
//内联函数
inline int Add(int x, int y)
{
int ret = x + y;
return ret;
}
若是写一个两位数相加的宏应该这样写。
cpp
#define Add(a, b) ((a) + (b))
远没有写内联函数容易。
在vs的debug下打开inline的方法:





这样操作即可。
当然,编译器不会无条件的展开我们创造的inline函数,原因是如果我们创建的inline函数过于庞大或者递归的太深,如果展开会浪费太多内存(假如我们的inline函数有100条指令,在项目中有10000处调用此函数,那全部展开就有10000*100条指令,但是不展开就只有10000+100条),编译器就会觉得我们不靠谱而不展开。
八、nullptr
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++的新语法,我们通过以上的内容就可以大致了解到了我们的C语言和C++的区别,在我们之后的内容中会更加详细的讲解C++的各种新语法和特性,我们拭目以待。
🎇坚持到这里已经很厉害啦,辛苦啦🎇
ʕ • ᴥ • ʔ
づ♡ど