文章目录
引言:
首先C的语法99%都适用于C++
C语言有很多缺陷,C++祖师爷为了补充缺陷所编写
一.命名空间:
首先看一下以下代码的问题:
这个报错的原因是rand重定义,在stdlib.h库中里面有rand,导致与全局变量rand冲突。
C语言并没有解决这个冲突,只能换个名字避免。
域作用限定符:
先提出个域的概念:全局域,局部域,命名空间域,类域。
全局域与局部域:
例如以下代码:
取的局部优先原则打印的1。
那要打印全局域的x=0就需要:域作用限定符“::”
域::x如果域没有内容,默认指的是全局域
命名空间域:
如果全局变量有多个x,那么就用到C++的命名空间域
可以看出两个x都能打印出来。
域的搜索优先级:
编译器搜索原则:不指定域:1. 当前局部域
2.全局域
指定域:直接去指定域搜索
验证如下:
顺便把先前的rand命名冲突解决了:
结构体用域表达比较特殊:
这就引出另一个问题,这样写多个命名空间域里面的变量就很麻烦,如下:
因此有一个办法可以解决,把命名空间域展开:
画图讲解一下:
在平时,只能通过域作用限定符“::”把命名空间域中的变量一个个“连接”,using namespace是直接把命名空间域的格子打开,随意使用其中变量。
但是,又出现了一个问题:在项目中是很少把命名空间域展开的,怕冲突太多。
所以用把某个成员名引入的语法:
例如如下:
总结:
命名空间的使用方法有三种:
1.加命名空间名称及作用域限定符:
int main()
{
printf("%d\n", N::a);
printf("%d\n", N::b);
return 0;
}
2.使用using将命名空间中某个成员引入
using N::b;
int main()
{
printf("%d\n", N::a);
printf("%d\n", b);
return 0;
}
3.使用using namespace 命名空间名称 引入
using namespce N;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
二.输入&输出:
输出:
C语言中打印都是printf
C++则是:
include <iostream>
using namespace std;
int main()
{
cout << "Hello world" <<endl;
return 0;
}
其中<<叫做流插入。
cout 是io流,其中c是控制台(console)的意思,out就是向其输出
控制台相当于windos的CMD,就是下面这个
endl相当于"\n",endline的缩写,例子如下:
流插入能够自动识别打印的是什么类型:
输入:
C语言中输入时scanf;
C++常用的是cin:
int main()
{
int i = 0;
cin >> i;
return 0;
}
其中 >>叫做流提取,并且也是自动识别类型
iostream在这里不在这里深入讲解
三.缺省参数:
以下代码为例:
可以看出Func()没有给出参数,则按照声明函数中的a = 0作为参数,给出Func(1)则啊= 1作为参数。
全缺省参数:
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;
}
注意:1. 半缺省参数必须从右往左依次来给出,不能间隔着给
2. 缺省参数不能在函数声明和定义中同时出现
反例1:
反例2:
||
可以看出声明和定义存在时,会报错,是因为如果声明和定义参数给的值不一样时,会产生歧义,所以祖师爷规定只能在声明里应用缺省参数。
原因:
以此代码作为例子讲解:
Stack.h | Stack.cpp | test.cpp |
---|---|---|
#include < iostream> using namespace std; void Func(int a, int b = 20, int c = 30); | #include “Stack.h” void Func(int a, int b, int c ) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } | #include “Stack.h” int main() { Func(1,2,3); return 0; } |
以上是代码执行过程缩减.。
假设:1.如果声明和定义函数都有缺省参数,可能会造成两者不一样,会出现分歧。
2.如果声明有缺省参数,定义无缺省参数,可以生成汇编然后向下进行。
3.如果声明无缺省参数,定义有缺省参数,编译就卡住了,无法向下进行。
故:在声明应用缺省参数,在定义不用。
再扩展一下最后链接的过程:
首先看反汇编:
可以看出call的不是函数的地址,jump是函数的地址
在形成汇编时:call (?),而不是call函数地址。在链接的时候才能在某一个文件中找到call函数的地址。(这里实力不佳,只知道结论,证明不出来)
四.函数重载:
C语言不许有同名函数
C++允许有同名函数,要求:函数名相同,参数不同,构成函数重载(这里要注意,必须在同一个域里面才能构成函数重载)
例子如下:
函数重载有三种:
1.参数类型不同;
2.参数个数不同
3.参数顺序不同(本质还是类型不同)
这里举例一下顺序不同
这里来谈一下C语言为什么不支持重载:
在链接时,C语言时直接用函数名去找地址,有同名函数的时候分不开。
C++有函数名修饰规则:名字中引入参数类型,各个编译器自己实现一套(命名规则没有做到统一)
这里说一下g++编译器的规则:_Z[函数名的长度][函数名][类型]
举个例子:
这里就是拿Linux上的g++编译器演示:
![]() | ![]() |
---|
gcc的编译器:
![]() | ![]() |
---|
在VS下演示(只写函数声明,不写函数定义可以显示出来):
![]() | ![]() |
---|
可以看到VS的编译规则跟gcc,g++的编译规则不一样
看个题:
这里选D,因为参数相同,只改变返回值的类型是不构成重载的
五.引用:
引用就是取别名,祖师爷嫌指针太麻烦想出的。
引例:
可以看出a,b的地址是一样的,就是名字不一样,就比如鲁迅是周树人也是鲁迅。
这里是交换的例子,语法是x是a的别名,y是b的别名,所以可以交换。这里先说语法,先不谈论底层。
注意:1.引用必须初始化。
2.引用定义后,不能改变指向。
例子:
初始化也是相同的道理:
综上所述:指针和引用的功能类似,重叠。
C++的引用,对指针使用比较复杂的场景进行一些替换,让代码更简单易懂,但是不能完全替代指针
引用不能完全替代指针的主要原因:引用定义后,不能改变指向。
函数中的引用:
1.做参数:
做参数时是做输出型参数。
扩展:
输入型参数就是传的拷贝,不可以改变 | 输出型参数是在作函数参数的时候,是可以改变的 |
---|---|
![]() | ![]() |
2.做返回值:
首先是返回值为int类型,存储a的栈虽然会销毁,但是a的值会传到寄存器里,然后赋值给ret。
这里返回值为别名,a的别名也是a,a在栈中,所以在调用函数后栈会销毁,a的空间就没了,相当于“野别名”,ret = 原先存a的地址存的数据,有的编译器会报错。
返回值是别名,返回的是栈里面的空间,ret是别名,所以只能指向a原先在的地址,也是个“野引用”,原先的空间一旦被覆盖,用ret很危险。
所以当返回变量为局部变量的时候,不能用引用。
基本都是栈用引用做返回值。
引用与指针的效率对比:
传参:
测试代码如下:
#include<iostream>
#include<stdio.h>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
void TestFunc1(A a)
{
}
void TestFunc2(A& a)
{
}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
可以看出引用的效率比指针快不少
返回值:
测试代码如下:
#include<iostream>
#include<stdio.h>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
A a;
// 值返回
A TestFunc1()
{
return a;
}
// 引用返回
A& TestFunc2()
{
return a;
}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
引用做返回值的效率比指针的效率低了不少
引用与指针的区别:
语法:引用时别名,不开空间,指针是地址,需要开空间存地址。
底层:引用底层是用指针实现的
![]() | ![]() |
---|
2.引用必须初始化,指针可以初始化也可以不初始化。
3.引用不能改变指向,指针可以。
4.引用相对更安全,没有空引用,但有空指针,容易出现野指针,但是不容易出现野引用
5.sizeof和++用法不一样。比如引用sizeof是类型的大小,指针是地址的大小
const引用:
我们发现是不能运行的,是因为10是具有常性,不可被更改 | 加个const即可 |
---|---|
![]() | ![]() |
又引出一个问题:
别名取不了,是因为权限放大,本来就只能只读,现在取完还想改,是不可行的 | 加个const即可 |
---|---|
![]() | ![]() |
关于函数传参一样:
发现传不过去,也是一样,权限放大,一旦传过去n不仅有读的权限还有改的权限 | 加个const即可 |
---|---|
![]() | ![]() |
以上都是权限放大的案例,权限缩小和平移是可取的:
六.内联函数:
引子:
假设调用100w次函数,就会建立100w个栈帧,然后再销毁,很麻烦。
int Add(int a,int b)
{
return a + b;
}
int main()
{
for (int i = 0 ;i<1000000;i++)
{
int ret = Add(1, 2);
}
return 0;
}
在C语言中,是用宏解决的
#define Add(a,b) ((a)+(b))
int main()
{
for (int i = 0; i < 1000000; i++)
{
int ret = Add(1, 2);
}
return 0;
}
但是宏的易错点很多:因为是宏是预处理阶段进行替换,所以没有分号,也不是函数,并且需要括号控制优先级。
缺点也很多:调试不出来,语法复杂,没有类型的安全检查。
很麻烦,C++祖师爷就发明了内联函数:
inline int Add(int a,int b)
{
return a + b;
}
int main()
{
for (int i = 0 ;i<1000000;i++)
{
int ret = Add(1, 2);
}
return 0;
}
定义:
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
这里可以验证inline是否建立栈帧:
1.在release模式下,查看编译器生成的汇编代码中是否存在call Add
2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2022的设置方式)
debug版本操作如下:
调用函数的反汇编 | 内联函数的反汇编 |
---|---|
![]() | ![]() |
可以看出直接相加了,没有调用函数。
缺陷:
大一点的函数就不适合运用内联函数,会造成代码膨胀。(这里是比较可执行程序)
代码膨胀带来的影响:我们平常下载游戏的时候会有安装包,安装包下载的就是可执行二进制程序,如果过于大下载的就慢。(就比如王者荣耀进去还要下载一会)
特性:
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,如果函数很长,就不会展开(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)
- 递归是不能用inline的,加了也会忽略掉
当然,也不能把所有函数都加上内联。例如:300行的函数加上内联,技术显得(你懂的) - 建议内联函数声明与定义不要分离,inline被展开,函数地址就没了,在链接时main函数想要调用call的地址就没有。
在解释之前我们看一个关于链接错误的例子:
Stack.cpp | Stack.h | 2_26test.cpp |
---|---|---|
![]() | ![]() | ![]() |
这里就会出现一个重定义的问题
因为.h里面包含着函数声明和定义,.h在预处理阶段的时候会展开,Add函数就重复包含了,符号表里有两个Add。
所以解决这个问题解决需要:
1.声明和定义分离。(就一个文件里面有Add的定义)
2.用static修饰函数,静态修饰链接属性,只会在当前文件可见。(底层就是不会进符号表)
3.内联函数,inline修饰的函数不会进符号表,直接在.h文件中的函数变为内联函数
所以:小函数用内联,大函数用静态
然后你就会发现很乱,内联函数的定义和声明写哪的问题。
错误示范:
Stack.cpp | Stack.h | 2_26test.cpp |
---|---|---|
![]() | ![]() | ![]() |
运行会发现有链接错误
因为inline的函数并没有进入符号表,所以在链接的时候call地址不知道在哪。
所以,只需要声明和定义一起写:
七.auto
auto是自动推导类型
例子:自动把k的类型识别成int
指针:可以看出p1,p2都一样
![]() | ![]() |
---|
引用也同理:
![]() | ![]() |
---|
这些都基本用不上,而且用多了找类型都麻烦,只有类型够长才用得上。
平常写函数类型 | auot写 |
---|---|
![]() | ![]() |
在auto不能做函数的参数,但能做auto的返回值(C++新标准支持做返回值)
这里说一下返回值,不建议用,因为很乱。
例子:函数无限叠加,不知道返回的是声明类型,得挨个看
for与auto
引例:关于遍历数组
#include <iostream>
using namespace std;
int main()
{
int array[] = { 1,2,3,4,5 };
for (int i = 0;i < sizeof(array)/sizeof(int);i++)//以前的写法
{
cout << array[i] << ' ';
}
cout << endl;
for (auto i : array)//现在的写法
{
cout << i << ' ';
}
return 0;
}
这里就用到迭代器的知识,依次取数组中的值赋值给i,自动迭代,自动判断结束
这里给数组的每个成员×2:之所以用引用,因为i是数组成员的复制
还有一个重要的是,迭代器不能迭代指针:(目前只需知道数组即可)
八.nullptr
引例:可以想想以下代码输出的什么
#include <iostream>
using namespace std;
void f(int x)
{
cout << "void f(int x)" << endl;
}
void f(int* x)
{
cout << "void f(int* x)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
出现的原因就是C++中define NULL的替换
所以得写成这样:
C++11认为这样不合适,所以用nullptr
并且sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同都是8