文章目录
- 😀1. 命名空间
- 😄1.1 命名空间定义
- 😄1.2 命名空间使用
- 😊2. 输入和输出
- 😉3. 缺省参数
- 🫠3.1 缺省参数概念
- 🫠3.2 缺省参数分类
- 😬4. 函数重载
- 🙄4.1 函数重载概念
- 🙄4.2 函数重载原理
- 😮5. 引用
- 😯5.1 引用概念
- 😯5.2 引用特点
- 😯5.3 使用场景
- 😯5.4 常引用
- 😯5.5 引用和指针区别
- 🫥6. 内联函数
- 😶🌫️6.1 概念
- 😶🌫️6.2 特性
- 🤗7. 语法糖
- 🤭7.1 auto关键字
- 🫡auto简介
- 🫡auto使用细则
- 🤭7.2 基于范围的for循环
- 🫡范围for的语法
- 🫡范围for的使用条件
- 🤭7.3 nullptr
😀1. 命名空间
在写C的代码时,会用到大量的变量,如果工程量一大,就有可能发生变量名冲突或者是使用某个变量名时,名字和库里面的一样,这样就会导致访问冲突。当代码全部总和的时候,改变量名就是一个头疼的问题,这里变量冲突了,那里变量冲突了,十分头疼。
于是在C++里面就引入了命名空间namespace
,来解决这种命名冲突问题。
😄1.1 命名空间定义
C语言中,{}
里面的叫做域,在同一个域里面,变量不能重复定义。
而在C++里面,定义命名空间,需要使用namespace
,后面跟空间的名字,然后接一对{}
即可,{}
中即为命名空间的成员。
namespace Test
{
//定义变量
int rand = 10;
//定义函数
int Add(int a, int b)
{
return a + b;
}
//定义类型
struct Node
{
struct Node* next;
int val;
};
//嵌套定义
namespace T1
{
int Sub(int a, int b)
{
return a - b;
}
}
}
😄1.2 命名空间使用
如果全局域和局部域的变量名一样,但是又想使用全局域这个变量该怎么办呢?
::
是C++里面的作用域限定符,::
的左边用于指定所属的命名空间或者类,如果左边是空白的,则代表访问全局域。
命名空间访问的方式有三种:
-
命名空间+域作用限定符
namespace T { int a = 10; } int main() { //命名空间+域作用限定符 printf("%d\n", T::a); return 0; }
-
空间某个成员引入
namespace T { int a = 10; int b = 20; } //空间域部分展开 //可理解为 将变量b暴露在全局域中 using T::b; int main() { //error a未展开,不可直接使用 //printf("%d\n", a); printf("%d\n", b); //20 return 0; }
-
空间域全部展开
namespace T { int a = 10; int b = 20; int Add(int a, int b) { return a + b; } } //展开命名空间域 using namespace T; int main() { printf("%d\n", a); printf("%d\n",T::b); printf("%d\n",Add(5, 23)); }
看过C++的一些代码,经常会看到这样的代码:
#include<iostream>
using namespace std;
C++的标准库中的大部分内容都定义在std
命名空间中,直接展开的好处就是书写比较方便;但是对于初学者来说,最好是不要全部展开,因为我们定义变量时,不知道这个名称是不是标准库里面的名称,会不会导致冲突,所以刚开始学习时,如果要用,前面加上std::
,明确指定使用的命名空间即可。
另外,C++的头文件现在都不加.h
后缀。在早期为了兼容C语言,保持了C的命名约定,即以.h
为后缀。但在引入标准库时,为了和C语言头文件区分,规定C++头文件不带.h
。
😊2. 输入和输出
大家都知道,咱们是特别有礼貌的,入门第一步一定是先打个招呼“Hello World”,那C++如何“打招呼”呢?
iostream
头文件定义了两个重要的输入输出流对象:cin
和cout
。
cin
:cin
是C++标准库中的输入流对象,用于从输入设备(如键盘)读取数据。通过cin
,可以接收用户的输入,并将输入数据存储到相应的变量中。cout
:cout
是C++标准库中的输出流对象,用于向输出设备(如控制台)输出数据。通过cout
,可以将数据、文本或变量的值输出到控制台或其他输出流中。
#include<iostream>
using std::cout;
using std::cin;
using std::endl;
int main()
{
int a = 10;
int number;
cout << "Hello World" << endl; //endl 是换行符
cout << a << endl;
cin >> number;
cout << "number = " << number << endl;
return 0;
}
其中
<<
在C语言表示的是移位操作符,但是在cout << "Hello World" << endl;
表示流插入运算符,在输出操作中,<<
运算符将数据从右侧操作数插入到左侧操作数所代表的输出流中。
>>
同理,表示流提取运算符,在输入操作中,>>
运算符从左侧操作数所代表的输入流中提取数据,并将其存储到右侧操作数所代表的变量中。
cout
相比printf
,cout
可以连续输出,而且会自动识别类型;cin
和scanf
同理(这些只是以目前的知识简单分析)。
printf
和scanf
的速度会比cout
和cin
要快,因为C++的IO流要兼容C语言,每次读的时候,都会检查是否有C语言的东西,所以在一定程度上会影响性能。
😉3. 缺省参数
🫠3.1 缺省参数概念
C语言里面调用函数时,有时候我们可能不知道要传多少参数,当时我们采用的是用一个宏,来设置默认值,但是这样就会导致,如果需要传参的时候,不能进行传参,不够灵活。
于是C++里面就引入了缺省参数这个概念:函数或方法定义中指定的具有默认值的参数。它们允许在调用函数时省略对应的参数,而函数将使用预先定义的默认值来代替。
这就好比吃饺子,不晓得吃多少合适,那就默认吃7个,不多也不少。
如果知道自己的量,比如说我能吃14个,那就直接上14个饺子,蘸点醋,吃完该上班上班,该上学上学。
void eatDumplings(int defaultE = 7)
{
cout << defaultE << endl;
}
int main()
{
eatDumplings(); //不知道吃几个
eatDumplings(14);//吃14个
return 0;
}
🫠3.2 缺省参数分类
-
全缺省
void Func(int a = 1, int b = 2, int c = 3) //每个参数都设置缺省值 { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func(); return 0; }
-
半缺省
void Func(int a, int b = 2, int c = 3) //a 需要给具体参数 { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; } int main() { Func(1); return 0; }
Tips:
- 设置半缺省参数时,必须从右往左,依次给出来,不能跳着给;传参时,只能从左往右,不能跳着传参。
-
缺省参数不能在函数声明和定义中同时出现。
这就好比,一山不容二虎,声明说默认为1,定义说默认为2,这该听谁的?
如果声明和定义分开的时候,在声明时就应该设置缺省参数,不能到定义的时候再来设置。因为程序如果要调用该函数,是先找到声明,有这个声明,再去查看定义。
😬4. 函数重载
🙄4.1 函数重载概念
中华文化博大精深,一句话可能有多个意思,例如:A心高气傲,“谁也看不上”,B的debuff叠满“谁也看不上”。这就是重载。
而C++里的函数重载,大概意思也是如此:在同一个作用域内,可以定义多个具有相同名称但参数列表不同的函数。函数重载允许使用相同的函数名执行不同的操作,具体取决于函数调用时提供的参数。
函数重载的特点如下:
- 函数名称相同;
- 参数列表不同(参数类型、参数顺序、参数个数);
- 返回类型不同不足以重载函数。
int Add(int a, int b) //参数列表: (int a, int b)
{
return a + b;
}
double Add(double a, double b) //参数列表: (double a, double b)
{
return a + b;
}
int Add(int a, int b, int c) //参数列表: (int a, int b, int c)
{
return a + b + c;
}
🙄4.2 函数重载原理
为什么C语言不支持这个重载,而C++可以支持呢?
C程序要执行的话,分为四个阶段:
- 预处理:对源代码文件进行宏展开、条件编译等操作,生成一个经过预处理的新文件(通常以
.i
或.ii
为扩展名)。 - 编译:检查语法,生成汇编代码。
- 汇编:将汇编代码转换成二进制机器码。
- 链接:将目标文件和可能的库文件合并在一起,解析符号引用,处理重定位信息,并生成最终的可执行文件。
在编译的时候,C语言编译器是直接调用的函数地址(Linux环境演示)
而C++编译器,会将函数的参数类型带进来
这就叫函数名修饰规则。
了解了这些,我们就能了解,为什么返回类型不一样不构成函数重载,因为返回类型并没有参与这个规则。
如果对编译链接这块有疑惑,可查看此篇文章:被隐藏的过程
😮5. 引用
😯5.1 引用概念
在C语言中,我们可以通过指针间接的访问和修改指向的对象,这个指针相当于中间商,中间商肯定是要赚点“差价”的,这个“差价”就是指针也需要占用内存空间。
还记得刚学指针的时候,对于这些概念,痛苦不堪。可能祖师爷也感觉这样麻烦,不如干脆一点,直接一对一,不着中间商了。
于是引入了引用的概念:提供了一个变量或对象的别名,可以通过引用直接访问和操作该变量或对象。
这就好像给人家取外号一样,“小甜甜”、“牛夫人”都是铁扇公主,只是叫法不一样而已。
😯5.2 引用特点
- 初始化: 引用在定义时必须进行初始化,并且一旦初始化完成后,它将一直引用同一个对象,无法重新指向其他对象。
- 无地址: 引用本身不占用内存空间,它只是作为已存在对象的别名,因此没有自己的地址。
- 专一: 引用只能有一个实体,而一个实体可以有多个引用。
void TestRef()
{
int a = 0;
int A = 1;
int& B = a;
int& b = a;
int& c = b;
//int& d; error
}
Tips:
引用类型必须和实体是同种类型
😯5.3 使用场景
-
做参数
void Swap(int& a, int& b) //传引用过去 { int tmp = a; a = b; b = tmp; } int main() { int a = 1; int b = 2; cout << "a = " << a << endl; cout << "b = " << b << endl; Swap(a, b); cout << "a = " << a << endl; cout << "b = " << b << endl; return 0; }
-
做返回值
int& func() { static int n = 1; //静态变量 return n; } int main() { int ret = func(); cout << ret << endl; return 0; }
下面这段代码和上面的有什么区别呢?
int& func() { int n = 1; return n; } int main() { int& ret = func(); cout << ret << endl; cout << " " << endl; cout << ret << endl; return 0; } //输出结果: // 1 // 随机值
第一段代码的
n
是静态变量,静态变量是存放到静态区中,函数栈帧的销毁,这个变量依然存在,不影响使用。而第二段代码的
n
是普通变量,该函数使用完毕之后,栈帧销毁,这个变量也随之销毁。我们第一次输出是正常结果,不同编译器会做出不同的处理,博主这里的用的是VS2022,该编译器函数空间被释放之后,并没有进行刷新;当我们在调用依次输出函数时,这时就会重新压栈,替代之前的func
所在的空间,所以第二次会显示随机值。Tips:
函数运行结束之后,我们所说的“销毁”,并不是该空间不存在了,而是说对应的空间被系统回收。就好比去图书馆借书,我们阅读完毕之后还是要还给图书馆,但是这本书,本身还是存在的。
不管是做参数还是做返回值,在一定程度上可以提高大型对象的传递效率。
但是返回引用时要确保被引用的对象仍然有效,否则会引发未定义行为。因此,在返回引用时需要谨慎处理,确保被引用的对象在函数生命周期内保持有效。
😯5.4 常引用
int main()
{
//× 引用过程中,权限不能放大
const int a = 1;
//int& b = a; //error
//√ c拷贝给d,权限没有放大,d的改变不影响c
const int c = 1;
int d = c;
//√ 引用过程中,权限可平移或者缩小
int n = 0;
const int& m = n;
//隐式类型转换,会产生临时变量。临时变量具有常性
double dd = 1.11;
int ii = dd;
//int& rii = dd; //error
const int& dii = dd;
return 0;
}
😯5.5 引用和指针区别
在语法层面,引用就相当于一个“外号”,没有独立的空间。但在底层逻辑上,是有空间的,引用的底层还是用指针来实现的。
int main()
{
int a = 0;
//引用 不开空间
int& ra = a;
ra = 10;
//指针 开空间
int* pa = &a;
*pa = 20;
return 0;
}
汇编指令:
我们发现引用的底层和指针的底层是一样的。这是因为编译器可以将引用转换为指针来进行处理,引用在 C++ 中通常被编译器实现为指针的语法糖,这种转换可以在不改变代码行为的前提下提供更高效的实现。
我们现在看虽然在汇编上一样,但在更复杂的程序中或涉及更多操作时,它们的汇编代码可能会有所区别,这具体由编译器来进行转换。要理解的是指针和引用,是两种不同的概念,它们是存在一定区别的:
-
引用是一个别名,用于给已经存在的对象起另一个名称;
指针是一个变量,用于存储另一个对象的地址。
-
引用必须始终引用一个有效的对象,它不能为 null;
指针可以具有空值(null),表示它没有指向任何有效的对象。
-
引用一旦绑定到一个对象后,不能再重新绑定到其他对象;
指针可以在任何时候重新赋值,将指向不同的对象。
-
引用不占用额外的空间,它只是已经存在的对象的别名;
指针需要额外的空间来存储另一个对象的地址,并且需要手动管理内存,包括内存的分配和释放。
总体而言,引用提供了一种更加安全和方便的访问对象的方式,它不需要显式的解引用操作,并且可以避免空指针问题。指针则提供了更大的灵活性和底层控制能力。选择使用引用还是指针,取决于具体的需求和场景。
🫥6. 内联函数
😶🌫️6.1 概念
在写代码的时候,可能会遇到需要频繁调用一个函数,函数调用过程中,涉及到了栈帧的创建和销毁,以及相关的参数传递、返回地址保存等操作。这些操作会带来一定的开销。在C语言中,如果函数比较简单,那我们就可以用宏,来替代,这就避免了频繁创建销毁栈帧的过程:
int Add(int a, int b)
{
return a + b;
}
//宏替代
#define ADD(x,y) ((x)+(y))
int main()
{
for (int i = 0; i < 10000; i++)
{
cout << Add(i, i + 1) << endl;
cout << ADD(i, i + 1) << endl;
}
return 0;
}
但是这样的话,也存在一些潜在问题,无法调试检查,可维护性也较差,而且宏是直接展开,哪里是整体还需要加上括号,这样也不好控制。
于是C++中引入了内联函数的概念:一种在编译器编译阶段进行的优化手段,用于将函数调用处的代码替换为函数体的内容,从而避免了函数调用的开销。内联函数在C++中通过关键字 inline
来声明。
inline int Add(int a, int b)
{
return a + b;
}
//#define ADD(x,y) ((x)+(y))
int main()
{
cout << Add(1, 2) << endl;
return 0;
}
我们查看反汇编可以观察到区别:
Tips:
debug模式下因为要调试,所以内联并不会展开,需要对编译器进行设置(以VS2022为例):
😶🌫️6.2 特性
-
inline
是一种以空间换时间的做法,因为inline
会将函数体的代码直接展开到调用点,这可能导致代码量的增加。如果内联函数的函数体较大,会导致代码膨胀,增加了代码的存储和加载开销。 -
inline
对于编译器而言,只是一个建议,意思就是内联只是向编译器发出一个请求,编译器可以选择忽略这个请求,不同的编译器对于inline
的实现机制可能不同。这里的代码如果直接展开的话,相比正常来说效率是更低的,所以编译器忽略了此次请求。
-
inline
的声明和定义通常放在头文件中,不建议将其分离。因为inline
是直接展开,没有函数地址,就会导致链接不上。
🤗7. 语法糖
平时我们写代码就很难受了,要是写的代码又臭又长就更加难受了。于是在C++中,就添加了一些语法糖,简化了一些语法结构,减少代码的冗余,使代码简洁、易读。这样就让我们能够“苦中作乐”了。
🤭7.1 auto关键字
🫡auto简介
C语言中,有typedef
可以给类型取别名,但是typedef
当过多的类型别名存在时,阅读代码变得更加困难,因为读者必须去查找别名的定义以了解其实际类型。
早期的C++标准中,auto
关键字具有不同的含义,它被用于声明具有自动存储期的局部变量。然而,由于其模糊性和潜在的歧义,很少有人在实际编程中使用。
于是C++11中,标准委员会赋予了auto
全新的含义即:auto
不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto
声明的变量必须由编译器在编译时期推导而得。
int TestAuto()
{
return 10;
}
int main()
{
int a = 1;
auto b = &a;
auto c = 1.11;
auto d = 'x';
auto e = "Hello";
auto f = TestAuto();
//输出类型
cout << typeid(a).name() << endl; //推断类型:int
cout << typeid(b).name() << endl; //推断类型:int*
cout << typeid(c).name() << endl; //推断类型:double
cout << typeid(d).name() << endl; //推断类型:char
cout << typeid(e).name() << endl; //推断类型:char const *
cout << typeid(f).name() << endl; //推断类型:int
//使用auto定义变量必须初始化
//auto n; //error
return 0;
}
🫡auto使用细则
-
初始化
auto
关键字的类型推断是基于变量的初始化表达式。因此,确保变量的初始化表达式提供足够的信息,以便编译器可以准确推断出变量的类型。 -
auto
与指针和引用结合int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl; //int* cout << typeid(b).name() << endl; //int* cout << typeid(c).name() << endl; //int return 0; }
Tips:
用
auto
声明指针类型时,用auto
和auto*
没有任何区别,但用auto
声明引用类型时则必须 加&。当使用
auto
关键字推断引用类型时,需要注意引用的生命周期和可变性。 -
同一行定义多个变量
int main() { //√ 类型相同 auto a = 1, b = 2; //× 类型不同 //auto c = 1, d = 1.1; //error return 0; }
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
-
auto
不可推导的场景-
auto
不能作为函数的参数void TestAuto1() //无返回类型 { ; } void TestAuto2(auto x) //此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导 { ; }
-
auto
不能声明数组void TestAuto() { auto arr[] = { 1,2,3 }; //error }
-
🤭7.2 基于范围的for循环
🫡范围for的语法
在C语言中,我们变量数组通常采用以下方式:
int main()
{
int arr[] = { 1,2,3,4,5 };
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d\n", arr[i]);
}
return 0;
}
对于一个有范围的集合而言,由我们来说明循环的范围是多余的,有时候还会容易犯错误。
因此C++11中引入了基于范围的for
循环。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
//分为两部分:第一部分是范围内用于迭代的变量,第二部分表示被迭代的范围。
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " ";
}
Tips:
与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
🫡范围for的使用条件
- for循环迭代的范围必须是确定的;
- 迭代的对象要实现
++
和==
的操作。
🤭7.3 nullptr
#ifndef NULL
#ifdef __cplusplus //判断当前环境是否为C++编译环境
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
我们发现,在关于NULL
其实是一个宏定义,在C++环境中被定义为0,这就就会影响我们使用空指针的时候,可能会出现一些与我们意向相悖的结果:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(1); // f(int)
f(NULL); // f(int)
f((int*)NULL); // f(int*)
return 0;
}
因为要兼容C语言,所以在C和C++代码中都可以使用NULL
来表示空指针。
但这样就对于C++比较难受了,于是在C++11中,引入了nullptr
关键字,用来表示空指针。
Tips:
- 因为
nullptr
是C++11中引入的关键字,所以不需要包含头文件,C++程序中直接使用就行- 在C++11中,
sizeof(nullptr)
与sizeof((void*)0)
所占的字节数相同- 对于C++的代码,建议使用
nullptr
以获得更好的类型安全性和可读性。此外,C++标准库中的新功能和API也更倾向于使用nullptr
。
当然了,这些只是一部分,C++很苦,所以祖师爷在设计的时候,在许多地方放了甜甜的“糖”,这得在之后学习的过程中,我们慢慢摸索。
本篇文章讲的其实都是一些C++关于C的一些补充或者优化,C++在继承了C语言的基础上,引入了更多的特性和概念。
那么本次的分享就到这里,如果有用的话,希望三连支持一下,我们下期再见,如果还有下期的话。