目录
1.命名空间:
在我们写c++或者c时我们写的第一个程序应该就是hello world。写c时我们使用printf函数需要调用C语言自己的函数库inlude<stdio.h>,这样我们就可以使用printf函数将我们的字符串打印到我们的屏幕上面了。在我们的c++中我们的大佬们为了弥补C语言的一些不足添加了一些其他的库。于是我们就可以使用cout来打印我们的字符串。但当我们刚刚开始看见下面的代码时我们肯定都会好奇这个cout函数前面的std::是用来做什么的。这就是c++所引入的新事物———命名空间。命名空间是为了防止我们的命名冲突的。我们刚刚接触编程时所编写的代码都是短小的代码不会存在名字重复的现象,就算出现了改一改冲突的名字也就好了。但是在我们开始进行项目的编程是代码往往多而复杂,参与的人员肯定也多,命名重复的现象一定会出现,所以为了避免这个复杂的修改过程我们c++增加了命名空间这一定义。而我们的iostream库里面的函数就是全部放在我们的std命名空间中的。我们使用这个库可以使用std来展开我们需要的代码也可以使用using namespace std来展开库中全部的代码(这种方法在平时练习中可以使用但是在项目中最好不要使用)。
#include<iostream>
#include<stdio.h>
int main()
{
printf("hello world\n");
//使用std::展开cout和endl
std::cout << "hello world" << std::endl;
return 0;
}
//或者是用using namespace std直接展开iostream全部的代码
#include<iostream>
#include<stdio.h>
using namespace std;
int main()
{
printf("hello world\n");
cout << "hello world" << endl;
return 0;
}
我们还可以自己来创建命名空间,把我们的代码直接放入我们的命名空间中这样在工程中别人的代码就不会和我们的代码有命名冲突了。
//就比如我写了一个加法代码
int Add(int a,int b)
{
return a+b;
}
//我为了避免其他人也写了一个Add函数
//与我的函数冲突所以我给它加上一个命名空间
namespace myspace
{
int Add(int a,int b)
{
return a+b;
}
}
我们可以写多个命名空间我们的编译器会自动把名字相同的命名空间合并的。
2.c++输入输出
刚刚我们有说到cout和endl。他们两个就是我们c++新引入的。我们c++中的输出函数cout是将我们的对象输出到我们的默认输出流上的(也就是屏幕或者说是控制台)。输入则是cin将对象从默认输入流(键盘)中输入;我们有了输入与输出自然需要让他们去到我们想要他们去掉的地方于是就有了<<流插入运算符和>>流提取运算符,我现在对于他们的理解暂时只是理解为将数据通过<<插入我们的文件或屏幕中,>>则是把数据从我们的键盘或文件中读取数据。我们c++的输入输出也与C语言有所不同它们不需要手动控制格式他们会自动识别我们的数据类型进行分配。
int main()
{
int a;
float b;
scanf("%d %f", &a, &b);
printf("%d,%.2f\n", a, b);
std::cin >> a >> b;
std::cout << a<<',' << b << endl;
return 0;
}
如上我们在给a,b赋值时c++可以不用手动控制;
3.缺省参数
在C++中我们的函数传递可以缺省一些参数。
void Func(int a = 0)
{
cout<<a<<endl;
}
int main()
{
Func(); // 没有传参时,使用参数的默认值
Func(10); // 传参时,使用指定的实参
return 0;
}
当我们没有传参时就会使用我们的默认值a=0;
1>我们缺省参数需要注意只能从1右往左缺省也就是只能从右往左设置默认值(全缺省和半缺省问题)
2>缺省参数的默认值在声明和定义中同时存在时应该在声明中设置
//全缺省
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
//半缺省(不全就是半缺省并不是一半缺省)
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<<a<<endl;
cout<<"b = "<<b<<endl;
cout<<"c = "<<c<<endl;
}
4.引用
引用的意思其实就是给我们的变量(对象)取别名
void TestRef()
{
int a = 10;
int& ra = a;//<====定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
这里的a和ra指向的就是同一片空间,ra就是我们a的引用就是a的别名;
引用特性
1.我们的引用时需注意引用定义需要初始化,我们这里int& ra = a;就是在给我们的ra初始化;
2.一个变量可以有多个引用,我们可以int& rra = a;int& rrra = a;他们都是a的别名
3.引用不可变,就是说我们我们在定义了int& ra = a;之后不能再使ra成为其他对象的别名了;
const int a = 10;
//int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;
// int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;
double d = 12.34;
//int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;
引用在进行引用常量的时候我们还应该在引用之前添加const来修饰引用这是因为我们的权限不能被放大我们常量就应该用常量类型来修饰,我们引用普通变量的时候我们还是可以将引用用const来修饰这样来缩小我们的权限;就比如我们把一个double类型变量赋值给一个被引用的int类型变量
double a=11.1;(int &b=a;)(这个不可行)const int &b=a;可行
时我们会发生类型转化这个时候会有一个临时变量来储存我们的转换的数据而这个临时变量就是常量类型的,所以呢我们const修饰的引用可行但是无const的不可行;所以我们在使用引用时要注意权限的放大是不行的;
引用做返回值:
引用返回值的时候它会直接返回我们的返回值,因为我们引用就是取别名的意思(引用在底层实现上其实是和指针是一样的我们打开我们编译器的反汇编代码就可以明白了)所以我们返回值一定要是一个没有被销毁的存在的值;
int& Count()
{
static int n = 0;
n++;
return n;
}
入上面的代码我们的n就是一个返回后不会被销毁的值但是如果我们去掉了我们的static修饰之后这个代码就错误了,因为我们的n是Count空间中的局部变量在我们返回了函数值之后我们的空间就会被销毁掉;所以我们返回的数据虽然存在但是他是被非法所访问的,这片空间已经不属于我们了,我们不应该去使用它;(我的意思是这个n变量无法访问了但是它的值我们还是可以拿到的)
引用做参数:
我们可以使用引用做参数,这样比直接传递数据要高效;在我们学习数据结构链表的时候是不是有时候会看见书本上写插入节点时有写到void PushSlist(LinkList &L, int i, ElemType e);这里的LinkList&L就是运用了引用的方式直接将这头节点的指针之间引用然后当作参数来传参的;这样就避免了传递结构体的大量数据拷贝的消耗;
下面是我寻找到关于传值与传引用和返回值和返回引用性能测试:
引用与指针:
1. 引用其实在底层上是和我们指针是一样的,
2.但是引用也有一些和指针不一样的地方我们的引用它在定义的时候就需要初始化而我们的指针不一定需要初始化;
3.引用在sizeof上的大小是根具我们的引用对象来变化的但是我们的指针是固定的大小;
4.引用指向的对象是不可以变化的,但是我们指针指向是可以改变的;
5.指针拥有NULL指针但是我们的引用没有
6.引用的加减改变的是它所指向对象的给予的值,但是我们指针的加减改变的是指针所指向的地址
7.改变引用可以直接引用的对象但是指针需要解引用才可以改变它所指向地址的数据;
总之引用更为简单与安全但功能少一些但是我们的指针功能更强大但是更为复杂所以容易出错;
5.函数重载
在C语言中我们函数的名字是不能相同的,但是在我们的c++中函数的名字是可以相同的这样相同的函数就是重载函数,当然这个相同不是完全相同,我们函数的参数,参数顺序,参数类型其中之一不同时就可以形成重载函数;
那重载函数为什么在c++中是行的通的呢,我们的代码在形成程序前可以分为四个阶段,编译预处理,编译,汇编,链接,首先我们在预处理阶段会将代码的注释删除,将宏进行替换,还会将我们的头文件进行展开;然后就会进入下一步编译将我们的代码分析处理检查语法等将其转换为汇编代码;在进入下一步汇编,代码汇编阶段会制作一个符号表,然后将汇编代码转换为我们机器所认识的二进制指令;最后在链接阶段合并我们的段表,与我们符号表合并与重定位;这样就形成了我们的程序;主要原因就在于我们C语言和c++在汇编时形成的符号表中c++会给函数命名加以修饰将函数的参数也加入命名中使得函数可以同名;但C语言只对函数名进行了修饰;所以C语言不能实现重载函数功能但是c++可以;
6.内联函数
内联函数与我们的宏函数定义类似,我们可以理解为在我们的函数名前方加一个叫做inline的标志我们的编译器会去识别这个标志判断是否进行内联操作(也就是在汇编代码处(此处应该是编译阶段进行)展开我们的函数代码而不是去调用我们的函数)下面是我在网上找来的关于内联函数调用的图片
上面每个图中左边是C语言代码右边是我们的汇编代码(在编译阶段生成的代码),第一个图是我们没有用到inline标志的函数,第二个图我们给Add函数加上了inline标志使得它成为了内联函数,所以在这个汇编代码上并没有显示call(在我们的汇编代码中call标志调用的意思)所以这个函数并没有被调用但是它还是拥有着函数功能所以说明它在汇编代码中被展开了;
inline的特性:
a.inline是一种空间换取时间的做法在我们编译器同意使用内联时会将函数直接展开在汇编代码上这样就省去了函数调用的时间,但是会增加我们的汇编代码长度使我们的目标文件变大;
b.inline只是一个对于编译器的建议,如果编译器觉得你这个函数太长了太大了它会不采纳你的建议直接把inline忽略(递归就会直接忽略inline);
c.inline一般是不能够定义与声明分离的这会导致我们的函数被展开,函数无法被找到,使得我们链接产生错误;
之前我们说内联函数与宏函数有些类似,接下来我们再来补充一下关于宏的知识,宏它有什么优点和缺点呢,优点1.方便我们修改代码,可以用它来替换重复的代码2.宏函数是直接被替换的(在预处理阶段)所以它的速度更快;缺点1.它无法进行调试,错误排除难度变大了;2.它不会对我们的参数进行类型检查;3.它的可读性差,在代码处无法明确阅读出代码;
因为我们的宏拥有一些问题所以我们c++引入了inline来解决它的宏函数的问题;
7.auto
auto的作用就是可以自动为我们的变量匹配类型
int a = 10;
auto b = a;
auto c = 'a';
就比如这几句代码,我们这里的b就自动被匹配了int类型,我们的c自动被匹配了字符类型char,这就是我们auto类型其中的一个作用;我们C语言中的dypedef有替代代码的作用也可以用来进行类型替换但是它相比与我们的auto一定没有那么方便,并且我们需要在知道我们被替换变量类型时才好使用typedef所以我们auto是一个非常方便的关键字;
下面我们再对auto进行一些场景模拟
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
这里我们的a会自动被匹配为int*类型因为这里的x是地址,地址需要指针变量存储;对于b我们可以这样理解b变量前方加上了一个*所以表示它一定是指针类型的所以我们给予它的变量应该也要是指针类型的才可以,所以我们给它的赋的值是&x如果赋值x这个代码将会错误;c变量前面有着&所以c变量是引用我们auto自动为其匹配了引用类型;
auto还有一些需要注意的地方,它不能作为函数的参数类型,因为它无法被直接推导,因为在函数编译时期我们函数参数是没有传递的,所以它没有办法转换为汇编代码;还有函数中数组的auto使用需要注意;
auto还可以为for自动确定范围
for循环后的括号由冒号“ :”分为两部分:第一部分是范 围内用于迭代的变量,第二部分则表示被迭代的范围,对于数组而言,就是数组中第一个元素和最后一个元素的范围
int array[] = { 1, 2, 3, 4, 5 };
for(auto e : array)
cout << e << " ";
接下来我们就可以对e进行操作因为e表示的是arry数组里面的元素,修改e也会改变数组元素;
8.nullptr
C语言只有NULL字符没有nullptr所以使得我们代码对于空指针使用时会出现一些问题,因为我们的NULL在宏定义上有两个定义一个是0,一个是((void*)0)指针0所以我们引出了新的空指针表示nullptr;
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入 的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
好啦,以上就是我对于c++基础的一些知识总结,希望这些知识可以帮助到我们!
路在脚下,我们努力往前走便好啦!2023.5.25--2023.5.29