目录
::域作用限定符
在之前单纯的C语言中,同一个域里无法取得相同的名字这是很不方便的,在大型的工程中不同的程序员不可能做到所有变量都不相同,所以::域作用限定符应运而生。
例如:
这种命名方式是不被允许的,在同一个域里,不能命名两个一样的变量。
#include<stdio.h>
int x = 0;
int main() {
int x = 1;
printf("%d\n", x);
return 0;
}
所以我们可以向上面一样,将一个变量放在全局,一个放在局部。这样就不会报命名冲突的问题了,但是如果我们打印这个x变量,会发现屏幕上打印出来的是1。
为什么呢?🤔编译器会首先在局部域里找x,然后在去全局域里找,如果在局部域里找到了,自然就直接打印x了。
那么我们如果想要打印全局变量的这个0呢,我们可以这样:
#include<stdio.h>
int x = 0;
int main() {
int x = 1;
printf("%d\n", x);
printf("%d\n", ::x);
return 0;
}
这时候就体现出了::的作用,如果::的左边什么都没有,那就是默认全局域,所以编译器会在全局域里找变量x。
命名空间域(namespace):
😎是为了解决命名空间里命名冲突的问题。
#include<stdio.h>
namespace project1 {
int x = 1;
}
namespace project2 {
int x = 2;
}
int main() {
printf("%d\n", project1::x);
printf("%d\n", project2::x);
return 0;
}
我们如果想要命名同一个名字的话,我们就可以这样,用命名空间域来隔开,格式为:namespace+空间域名字。这样我们就可以用域空间操作符来指定使用变量名称了。
编译器搜索原则:
无指定:1、当前局部域 2、全局域
有指定:1、直接在指定域中搜索
命令空间域里可以是:1、变量,2、函数,3、结构体。
#include<stdio.h>
namespace project1 {
int x = 1;
}
namespace project2 {
//变量
int x = 2;
//函数
int Add(int a,int b){
return a + b;
}
//结构体
struct ListNode {
/*....*/
}LNode;
}
int main() {
printf("%d\n", project1::x);
printf("%d\n", project2::x);
project2::Add(1,2);
struct project2::ListNode phead;
return 0;
}
需要注意的是:使用命名空间域里的结构体时,域名要在struct后结构体名前。
namespace还有一个使用方法:
当我们需要频繁调用project2时,就需要一直调用,就会变得很麻烦,所以我们介绍namespace的第二种用法:
这个叫做展开命名空间,也就是说使用了这个之后,project2就被展开了,被展开成了全局变量,再使用这个命名空间域里的东西的时候,就可以不用再引用了。
被展开的域相当于全局变量,所以当有局部变量的时候依然会优先访问局部变量。
所以,当我们使用C++的时候,通常为了方便,会先写using namespace std,std就是C++的库,是为了方便调用C++库里面函数或者是关键字的。 (但是也仅仅是在我们平时的练习测试用的多,主要是为了方便)。
还有一种写法就是我可以展开这个库的一部分,也就是说:
就像这样,把几个常用的给展开,就可以了。
这时候有人要提出问题了,如果命名域的名字也冲突了怎么办?🤔
答:命名域允许嵌套,也就说如果命名域真的冲突了,我们也可以命明域进行嵌套来解决,而且如果我们想,我们可以无限的套娃下去。但一般我们嵌套两层就够用了。
可以看到这里我们在用的时候,就需要用两次::域作用限定符 。
流插入和流提取(C++的输入输出)
#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
cout<<"Hello world!!!"<<endl;
return 0;
}
说明:
1. 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件
以及按命名空间使用方法使用std。
2. cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<
iostream >头文件中。
3. <<是流插入运算符,>>是流提取运算符。
4. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。
C++的输入输出可以自动识别变量类型。
#include <iostream>
using namespace std;
int main()
{
int a;
double b;
char c;
// 可以自动识别变量的类型
cin>>a;
cin>>b>>c;
cout<<a<<endl;
cout<<b<<" "<<c<<endl;
return 0;
}
// ps:关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。因为C++兼容C语言的用法,这些又用得不是很多,所以如果输出输入浮点数,我们还是采用C语言来实现 。
std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
1. 在日常练习中,建议直接using namespace std即可,这样就很方便。
2. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对
象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 +
using std::cout展开常用的库对象/类型等方式。
缺省参数:
#include<iostream>
using namespace std;
void Fun(int a = 10, int b = 20, int c = 30) {
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main() {
Fun(1,2,3);
Fun(1,2);
Fun(3);
Fun();
return 0;
}
当我们来写一个函数的时候,如果我们传参了,那么就跟正常的一样,如果没传参,那么就会默认的用我们括号里面的值。
上面是缺省参数的四种形式,传参的时候如果我们想要修改其中的参数可以只传它的参数,没传参的就会默认括号里的值,但是不能进行跳跃传参,比如我们想只传b但是a,c还用默认值的话,这是办不到的。
还有一个是半缺省的形式, 像上面的b,我们可以不给默认值,但是这时候传参的话就必须传值了。
举一个实际的例子:
#include<iostream>
using namespace std;
struct Stack{
int* a;
int size;
int capacity;
};
void StackInit(struct Stack* ps) {
//ps->a = NULL;
ps->a = (int*)malloc(sizeof(int) * 100);
}
int main() {
struct Stack st1;
StackInit(&st1);
return 0;
}
我们假设初始化栈的时候,开辟栈的大小使用malloc,但是当我们有以下三种情况的时候:
这种情况下我们又怎么进行动态开辟呢,大小该给多少呢?
这时候我们的缺省参数就派上用场了:
#include<iostream>
using namespace std;
struct Stack{
int* a;
int size;
int capacity;
};
void StackInit(struct Stack* ps ,int n = 4) {
//ps->a = NULL;
ps->a = (int*)malloc(sizeof(int) * n);
}
int main() {
struct Stack st1;
//确定要插入100个数据
StackInit(&st1,100);
struct Stack st2;
//确定要插入10个数据
StackInit(&st2,10);
struct Stack st3;
//不知道要插入多少的时候
StackInit(&st3);
return 0;
}
这样是不是就很方便呢。
那当声明与定义分离的时候,该在哪里给缺省参数呢?
答案是:声明。
首先声明和定义肯定是不能同时给缺省参数的,因为当两个缺省参数不同时,就会报错。
其次,缺省参数是在调用的时候用的,那么将缺省参数放在声明才不会在编译的时候报错。
最后,缺省值必须是常量或者全局变量。
函数重载:
C语言中我们不能命名相同名字的函数,在C++中我们可以命名相同函数的名字,但前提是:函数的参数的个数,类型,顺序不一样。
C语言做不到的,C++又是如何做到的呢?
C++明确了在调用的时候,不仅仅可以用函数的名字,还可以加上函数的参数类型合并起来在进行判断是否是重名的,并且还加上了参数数量的比较,就比如上面的两个Add,他们在编译完之后一个是Addci,另一个是Adddd。所以最后他们就是不同的函数。
但这里只是一个例子,不同的编译器会有不同的实现方式,但是思路都是一样的。
引用:
C语言中,我们如果想要指向一个变量的话,我们会将他的地址取出来存到指针变量里面,但是在针对一些复杂指针的时候,就很不方便,所以C++采用了引用。
#include<iostream>
using namespace std;
int main() {
int a = 1;
//引用
int& b = a;
cout << a << endl << b << endl;
return 0;
}
引用的形式就是:&(取地址符),如上面代码int& b = a,此时b就是a的别称,例如周树人的笔名是鲁迅,那么鲁迅和周树人都是指同一个人。所以引用的作用,就是给a取了一个别名b,那么无论输出a还是输出b,结果都是1。
#include<iostream>
using namespace std;
//这是我们C语言中的交换函数,必须传递实参的地址。
void Swap(int* x, int* y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
//这是用引用的写法,这里的x,y就是两个实参的别名
void Swap(int& x, int& y) {
int tmp = x;
x = y;
y = tmp;
}
int main() {
int a = 1;
int c = 5;
cout << a <<"<->" << c << endl;
//Swap(a, c);
Swap(&a, &c);
cout << a << "<->" << c << endl;
Swap(a, c);
cout << a <<"<->" << c << endl;
return 0;
}
交换函数正常的实现,这就是引用的用法。
注意事项:
1、引用必须初始化,不可以写成:
int& b;
b = c;
2、引用定义后不能改变指向,不能写成:
int& b = a;
int c = 3;
b = c; //这里就不是改变指向,而是赋值
3、 一个变量可以有多个引用,多个别名
int main() {
int a = 1;
int& b = a;
int& d = a;
int& e = a;
return 0;
}
这里面的bde都是a的别名。
既然引用的作用与指针作用重叠。
那么有小伙伴要提问了,那引用不是就替代指针了?🤔🤔🤔
C++的引用是为了解决指针使用复杂场景的一些替换,让代码更简单易懂,但不能完全代替。
引用不能替代指针的原因:引用定义后不能改变指向。
例如:
在双向链表中,删除中间的结点,该删除结点的前一个和后一个要改变指向,这是引用做不到的。
引用还有更为广泛的应用就是:
void PushBack(struct ListNode** phead,int x) {
//........
}
void PushBack(struct ListNode*& phead, int x) {
//........
}
int main() {
struct ListNode* pList = NULL;
}
假如我们在PushBack函数里要对指针进行修改,那么我们就必须传一个二级指针(如第一个写法),那有了引用之后(第二个写法)ListNode*phead就是ListNode*pList的一个别名,所以当然可以在PushBack函数进行修改了。
还有一种经常出现在书上的写法让一些刚学C语言的小伙伴们十分头疼:
typedef struct ListNode {
ListNode* prev;
ListNode* next;
int val;
}LNode, *PNode;
void PushBack(PNode& phead, int x) {
//........
}
int main() {
PNode pList = NULL;
return 0;
}
这个*PNode就是struct ListNode的指针,所以指针类型就直接是PNode,然后再结合上&引用,就实现了这个的参数类型的写法。
然后我们来一个简易的顺序表感受一下C和C++的不同:
#include<iostream>
#include<assert.h>
using namespace std;
struct SeqList {
int* a;
int size;
int capacity;
};
void SLInit(SeqList& sl) {
sl.a = (int*)malloc(sizeof(int) * 4);
sl.size = 0;
sl.capacity = 4;
}
void SLModity(SeqList& sl, int pos,int x) {
assert(pos >= 0);
assert(pos < sl.size);
sl.a[pos] = x;
}
void SLPushBack(SeqList& sl, int x) {
sl.a[sl.size++] = x;
}
int SLGet(SeqList& sl, int pos) {
assert(pos >= 0);
assert(pos < sl.size);
return sl.a[pos];
}
int main() {
SeqList sl;
SLInit(sl);
SLPushBack(sl, 1);
SLPushBack(sl, 2);
SLPushBack(sl, 3);
SLPushBack(sl, 4);
for (int i = 0; i < sl.size; i++) {
cout << SLGet(sl, i) <<" ";
}
cout << endl;
for (int j = 0; j < sl.size; j++) {
int val = SLGet(sl, j);
if (val % 2 == 0) {
SLModity(sl, j, val * 2);
}
}
for (int i = 0; i < sl.size; i++) {
cout << SLGet(sl,i) << " ";
}
return 0;
}
这个代码嵌了一点点C++,但是跟C差不多。
#include<iostream>
#include<assert.h>
using namespace std;
struct SeqList {
int* a;
int size;
int capacity;
void Init() {
a = (int*)malloc(sizeof(int) * 4);
size = 0;
capacity = 4;
}
void PushBack(int x) {
a[size++] = x;
}
int& Get(int pos) {
assert(pos >= 0);
assert(pos <= size);
return a[pos];
}
};
int main() {
SeqList sl;
sl.Init();
sl.PushBack(1);
sl.PushBack(2);
sl.PushBack(3);
sl.PushBack(4);
for (int i = 0; i < sl.size; i++) {
cout << sl.Get(i) <<" ";
}
cout << endl;
for (int j = 0; j < sl.size; j++) {
if (sl.Get(j) % 2 == 0) {
sl.Get(j)*= 2;
}
}
for (int i = 0; i < sl.size; i++) {
cout << sl.Get(i) << " ";
}
return 0;
}
这里如果用int不加引用的话,相当于返回的是一个临时拷贝,临时变量是建立在堆上的,并且临时变量具有常性(可以理解为被const修饰过的),那么我们肯定是没办法修改的。就算这里不考虑常性,那修改的也只是这个临时变量,并没有修饰到顺序表中的本体,但是加了引用之后,不产生临时变量,引用就相当于返回了他的别名,那么修改别名就是修改了本体。所以用引用返回值具有有了读写功能。
引用和指针的区别:
语法层面:
1、引用是别名,不开空间,指针是地址需要开空间。
2、引用必须初始化,指针可以初始化也可以不初始化。
3、引用不能改变指向,指针可以。
4、引用相对更安全,没有空引用,但是空指针,容易出现野引用,但是不容易出现野指针。
5、sizeof、++、解引用访问等上面的区别。
底层:
汇编层面上,没有引用只有指针,编译后引用也变成了指针。
在汇编层面上,引用和指针是没有区别的。
内联函数:
C语言调用函数是需要开销的,假如说C语言有一个频繁调用的小函数:
int Add(int a, int b) {
return a + b;
}
假如这个相加的小函数需要调用100w次,那就需要建立100w个栈帧空间。
那C语言是如何解决这个问题的:宏函数。
#define Add(a,b) ((a)+(b))
但是宏有很多缺点:
1、语法复杂,坑较多,不容易控制。
2、不能调试。
3、没有安全类型的检查。
所以C++引出了内联函数:inline
inline int Add(int a, int b) {
return a + b;
}
他的功能其实是和宏差不多,在多次调用的时候,不需要每次都建立栈帧。
并且优化了宏的缺点 。
inline函数的特性:
1、内联函数是一种空间换时间的做法,如果编译器把函数当作内联函数使用的时候,在编译过程中,编译器会把函数体替换为函数调用。缺点:函数体展开使目标文件变大。(当函数体较长,再去多次调用多次展开,会出现代码膨胀)优点:减少函数调用开销,提高代码运行效率。
2、将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰。
3、inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);
// F.cpp
#include "F.h"
void f(int i)
{
cout << i << endl;
}
// main.cpp
#include "F.h"
int main()
{
f(10);
return 0;
}
// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
//f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
在C++中是比较排斥使用宏的,一般会采用以下方法来替代宏:
1、常量定义换用const、enum
2、短小函数定义换用inline
auto关键字:
1、类型思考:
随着我们学习的深入,变量的类型会越来越复杂,这就导致我们在使用的时候变得十分麻烦。
#include <string>
#include <map>
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" },
{"pear","梨"} };
std::map<std::string, std::string>::iterator it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。
2、auto介绍:
auto 作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。言简意赅就是auto可以根据变量的值自动推导变量的类型。
int TestAuto()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = TestAuto();
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化
return 0;
}
【注意】
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型 。
auto的使用细则 :
1. auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{
int x = 10;
auto a = &x;
auto* b = &x;
auto& c = x;
cout << typeid(a).name() << endl;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
*a = 20;
*b = 30;
c = 40;
return 0;
}
2. 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
void TestAuto()
{
auto a = 1, b = 2;
auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
}
3、auto不能使用的场景
1. auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
2. auto不能直接用来声明数组
void TestAuto()
{
int a[] = {1,2,3};
auto b[] = {4,5,6};
}
基于范围的for循环(C++11)
1 、范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
array[i] *= 2;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
cout << *p << endl;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for(auto& e : array)
e *= 2;
for(auto e : array)
cout << e << " ";
return 0;
}
范围for相当于是依次取数组里面的值赋值给e,然后再将e输出,auto也是能更好的自动识别类型,以应对各种类型的数组,&是为了如果想要改变数组内容,那么我们引用就把e变成了数组每个元素的别名,就可以修改数组里面的值了。
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
上面这个代码是不能用范围for的,现阶段范围for只能用于数组,但是在上面的TestFor函数中,array不是数组,而是一个指针。
C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
// ……
}
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
所以从C++11以来用nullptr来填上了这个小坑。
#include<iostream>
using namespace std;
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
f(nullptr);
int* p = nullptr;
return 0;
}
注意:
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。