【C++】C++基础
目录
一. C++关键字(C++98)
二、C++的第一个程序
三、命名空间
3.1.namespace的价值
3.2.namespace的定义
3.2.命名空间使用
总结:在项目当中第一、第二种方法搭配使用,第三种冲突风险非常大,仅适合练习使用。
四、C++输入&输出
五、缺省参数
六、函数重载
七、引用
7.1.引用的概念和定义
7.2.引用的特性
7.3.引用的使用
7.4.const引用
7.5.指针和引用的关系
八、 inline
九、nullptr
一. C++关键字(C++98)
C++总计63个关键字,C语言32个关键字。
下面只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面对应文章会详细讲解。
asm | do | if | return | try | continue |
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_cast |
二、C++的第一个程序
C++兼容C语言绝大多数的语法(部分地方存在改动),所以C语言实现的那套hello world依旧可以在C++下运行,如下图
// test.cpp
#include<stdio.h>
int main()
{printf("hello world\n");return 0;
}
在实际应用中,以VS为例,当文件后缀改为.cpp后,vs编译器看到是.cpp就会调用C++编译器编译,当文件后缀改为.c后,vs编译器就会调用C编译器编译;但是linux下要用g++编译,不再是gcc。
当然C++有一套自己的输入输出,严格说C++版本的hello world应该是这样写的。
// test.cpp
// 这里的std cout等都看不懂,没关系,下面会依次讲解
#include<iostream>
using namespace std;int main()
{cout << "hello world\n" << endl;return 0;
}
三、命名空间
3.1.namespace的价值
在C/C++中,同一域内不能存在定义相同名字的不同事物,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域或者局部作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
如下图,我们定义了一个变量rand,如果我们不包含的头文件<stdlib.h>,可以直接使用,但是由于在这个头文件内,rand是被定义成一个函数,那么我们定义的rand就会与头文件中的rand发生冲突,产生重定义错误。
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{// 编译报错:error C2365: “rand”: 重定义;以前的定义是“函数”printf("%d\n", rand);return 0;
}
看上去来说,这种情况我们似乎只要使用不同的名字就可以避免,但是在实际的项目工程中,不同人员之间,开发者与使用者之间,默契的使用不同的名称是完全不可能的,
简单的名称也无法满足巨大的需求,因此对于C语言来说这样的命名冲突是普遍存在的问题,C++引入namespace就是为了更好的解决这样的问题
3.2.namespace的定义
• 定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
namespace zlr
{// 命名空间中可以定义变量/函数/类型int rand = 10;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};
}
• namespace本质是定义出一个域(命名空间域),这个域跟全局域各自独立,不同的域可以定义同名变量,所以下面的rand不在冲突了。
我们上文说过,C/C++中不同的域中是不可以定义相同的变量的,原本C/C++中内只有全局域与局部域,大量的变量、函数、类挤在两个域内,会产生大量的冲突。而使用namespace+命名空间名就相当于在变量、函数、类周围建造了一堵墙,形成了命名空间域域其他域相互隔绝,命名空间名不同,虽然同属于命名空间域,但是不同墙之间也会隔绝,冲突大大减少。(注:实际项目实践中,不同的项目组之间会分配固定的命名空间名,因此命名空间域定义冲突的可能性非常低,如果冲突。)
• C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
以上图为例,C语言中,当局部与全局定义相同的变量时,查找逻辑是就近原则,先采用局部定义的变量。对于命名空间域来说,在变量周围建墙后,查找时只会在其他域在全局域中找。想要使用命名空间域内定义,有一定的要求。
• namespace只能定义在全局,当然他还可以嵌套定义。
//2. 命名空间可以嵌套
namespace zlr
{namespace z1{int rand = 1;int Add(int left, int right){return left + right;}}namespace z2{int rand = 2;int Add(int left, int right){return (left + right) * 10;}}
}
int main()
{
//……………………return 0;
}
• 项目工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。
// 多文件中可以定义同名namespace,他们会默认合并到一起,就像同一个namespace一样
// Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace zlr
{typedef int STDataType;typedef struct Stack{int top;int capacity;}ST;void STInit(ST* ps, int n);void STDestroy(ST* ps);void STPush(ST* ps, STDataType x);void STPop(ST* ps);STDataType STTop(ST* ps);int STSize(ST* ps);bool STEmpty(ST* ps);
}
// Stack.cpp
#include"Stack.h"
namespace zlr
{void STInit(ST* ps, int n){assert(ps);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;}// 栈顶void STPush(ST* ps, STDataType x){assert(ps);// 满了, 扩容if (ps->top == ps->capacity){printf("扩容\n");int newcapacity = ps->capacity == 0 ? 4 : ps->capacity* 2;STDataType* tmp = (STDataType*)realloc(ps->a,newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}ps->a = tmp;ps->capacity = newcapacity;}ps->a[ps->top] = x;ps->top++;}//...
}
// Queue.h
#pragma once
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace zlr
{typedef int QDataType;typedef struct QueueNode{int val;struct QueueNode* next;}QNode;typedef struct Queue{QNode* phead;QNode* ptail;int size;}Queue;void QueueInit(Queue* pq);void QueueDestroy(Queue* pq);// 入队列void QueuePush(Queue* pq, QDataType x);// 出队列void QueuePop(Queue* pq);QDataType QueueFront(Queue* pq);QDataType QueueBack(Queue* pq);bool QueueEmpty(Queue* pq);int QueueSize(Queue* pq);
}
// Queue.cpp
#include"Queue.h"
namespace zlr
{void QueueInit(Queue* pq){assert(pq);pq->phead = NULL;pq->ptail = NULL;pq->size = 0;}// ...
}
// test.cpp
#include"Queue.h"
#include"Stack.h"
// 全局定义了一份单独的Stack
typedef struct Stack
{int a[10];int top;
}ST;
void STInit(ST* ps) {}
void STPush(ST* ps, int x) {}
int main()
{// 调用全局的ST st1;STInit(&st1);STPush(&st1, 1);STPush(&st1, 2);printf("%d\n", sizeof(st1));// 调用zlr namespace的zlr::ST st2;printf("%d\n", sizeof(st2));zlr::STInit(&st2);zlr::STPush(&st2, 1);zlr::STPush(&st2, 2);return 0;
}
• C++标准库都放在一个叫std(standard)的命名空间中。
为了防止冲突,设计者当初设计之际也将语言本身自带的库放在std命名空间内,需要注意的是不同的库是放在不同的文件内,正是因为上文所讲的相同的namespace会被视为一份,所以当我们包含不同头文件时不会冲突(包含头文件后,也需要对应的方法才能使用头文件命名空间内的定义)。
3.2.命名空间使用
编译查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间里面去查找。所以下面程序会编译报错。所以我们要使用命名空间中定义的变量/函数,有三种方式:
• 通过域作用限定符(::)指定命名空间访问,项目中推荐这种方式。
域作用限定符写在对应函数或变量等名字左边,如上图,对于遭遇就近原则的变量,我们在其左边加上::(访问限定符左边加不加空格都不影响),那么变量采用的就是全局定义的值。
namespace zlr
{int rand = 10;
}int a = 0;int main()
{printf("%p\n", rand);printf("%d\n", zlr::rand);int a = 1;printf("%d\n", a);// ::域作用限定符printf("%d\n",::a);return 0;
}
我们要使用命名空间内定义的值,我们需要命名空间名+::+变量的形式,这样在编译环节,就相当于拆除对应的墙,可以进对应命名空间找到对应的量。
// 域
namespace zlr
{int rand = 10;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};
}int main()
{printf("%d\n", zlr::Add(1, 1));struct zlr::Node p1;return 0;
}
对于命名空间内定义的函数和结构体,我们以如上图的形式使用,需要注意的是对于结构体,::右边接结构体名,因为结构体名才是我们实际定义的量,struct是关键字。
#include <stdio.h>
#include <stdlib.h>// 域
namespace zlr
{namespace z1{int rand = 1;int Add(int left, int right){return left + right;}}namespace z2{int rand = 2;int Add(int left, int right){return (left + right) * 10;}}
}int main()
{printf("%d\n", zlr::z1::rand);printf("%d\n", zlr::z2::rand);return 0;
}
对于嵌套定义的命名空间,我们就需要一层一层的“拆墙”,才能使用最内部定义的对应量。
•通过 using(关键字)将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。
对于命名空间内的定义的量,每次使用都需要以上文第一种的方式来使用,无疑非常繁琐,我们最初使用命名空间的目的是防止命名冲突的问题,如果一个项目,我们能确保某一个量名字不会冲突或者冲突概率很小,并且这个量多次使用,那么我们就可以通过using+命名空间名+对象名形式,将对应量完全展开(其他量不受影响),这样量继续相当于全局定义的,可以直接使用。
#include <stdio.h>
namespace zlr
{int a = 0;int b = 1;
}using zlr::a;int main()
{printf("%d\n", a);printf("%d\n", zlr::b);return 0;
}
• 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。
除此之外,对于学生来说参加一些比赛或者写一下小算法的情况,可以确保一定不会遇到命名冲突的问题,并且需要经常使用std内的定义量,所以我们可以通过using+namespace+命名空间名的形式将对应命名空间完全展开,这样该命名空间内的所有量可以直接使用。
//展开头文件
#include <stdio.h>
namespace zlr
{int a = 0;int b = 1;
}using namespace zlr;int main()
{printf("%d\n", a);printf("%d\n", b);return 0;
}
总结:在项目当中第一、第二种方法搭配使用,第三种冲突风险非常大,仅适合练习使用。
四、C++输入&输出
• <iostream> 是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
类似于我们所熟知的C语言内的<stdio.h>为了使用C++内的输入输出,以后我们也需要开头包含该头文件
#include <iostream>
• std::cin 是 istream 类的对象(类本文不做详细介绍,可以先理解为结构体),它主要面向窄字符(narrow characters (of type char))的标准输入流(类似scanf)。
• std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流(类似pritntf)。
(注:c代表的就是字符的意思,除了窄字符还有宽字符,宽字符本文暂且不详细介绍。
内存中为了方便计算数据被划分出int等各种类型,但是在文件、网络等内部都是以字符流的形式存储。)
• std::endl 是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。(底层也较为复杂,使用了运算符重载,本文不做具体讲解,可以理解为换行符)
• <<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移,这里是通过运算符重载实现的一符多用,本文详细不介绍运算符重载)。
• cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用他们。
• 一般日常练习中我们可以using namespace std,实际项目开发中不建议using namespace std。
#include<iostream>
using namespace std;int main()
{int a = 0;cin >> a;cout << "hello world\n" << endl;//完全展开std,直接使用std::cout << "hello world\n" << std::endl;//不展开std的使用方式return 0;
}
上段代码,cin这一段,cin是通过标准输入设备(键盘)通过>>提取输入的字符自动转换为对应的
int类型数据放到a中,cout这一行,cout代表着标准输出(这是代表控制台——运行代码常见的黑框框),然后"hello world\n" 通过<<被提取输出到控制台,endl(end line)通过<<再输出一个换行符,并刷新缓冲区。
• 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,本文不做具体介绍),其实最重要的是C++的流能更好的支持自定义类型对象的输入输出。
#include<iostream>
using namespace std;int main()
{int a = 0;double b = 0.1;char c = 'x';cout << a << " " << b << " " << c << "\n" << '\n' << endl;//输出多个空格std::cout << a << " " << b << " " << c << std::endl;//输出单个空格//cout输出不需要指定输出格式,自动识别类型//scanf("%d%lf", &a, &b);//printf("%d %lf\n", a, b);double d = 2.22222222;printf("%.2lf\n", d);cout << d << endl;cout << &d << endl;// cin可以自动识别输入变量的类型//cin >> a;//cin >> b >> c;cin >>a>> b >> c;cout << a << endl;cout << b << " " << c << endl;return 0;
}
需要注意的是使用<<时,因为<<是二元操作符,像上图这样写就存在a、空格、cout三个操作数。只能一个一个输入。
需要注意的是:C++中想实现输出数据精度控制的功能非常复杂,这里推荐使用C语言printf来实现精度的控制
• 这里我们没有包含<stdio.h>,也可以使用printf和scanf,因为在包含<iostream>间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
#include <iostream>
using namespace std;
int main()
{int a = 0;double b = 0.1;char c = 'x';cout << a << " " << b << " " << c << endl;std::cout << a << " " << b << " " << c << std::endl;scanf("%d%lf", &a, &b);printf("%d %lf\n", a, b);// 可以自动识别变量的类型cin >> a;cin >> b >> c;cout << a << endl;cout << b << " " << c << endl;return 0;
}
#include<iostream>
using namespace std;
int main()
{// 在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下3行代码// 可以提高C++IO效率ios_base::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);return 0;
}
因为C++很多地方都要兼顾C,那么就会需要兼顾C的缓冲区的刷新等问题,在面对大量的IO需求的场景下(如竞赛)通常会加上如上中的三行代码来提高效率(本文仅为入门基础,具体原理不详细介绍)
• IO流涉及类和对象,运算符重载、继承等很多面向对象的知识,这些知识我们还没有讲解,所以这里我们只能简单认识一下C++ IO流的用法,后续文章详细介绍IO流库。
五、缺省参数
• 缺省参数是声明或定义函数时为函数的参数指定一个缺省值(常量或者全局变量)。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
• 全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
• 带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
#include <iostream>
using namespace std;
// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)//全缺省
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;
}
// 半缺省
void Func2(int a, int b = 10, int c = 20)//半缺省
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;
}
int main()
{Func1();//使用缺省参数设置的默认值Func1(1);Func1(1, 2);//依次赋值Func1(1, 2, 3);Func2(100);Func2(100, 200);Func2(100, 200, 300);return 0;
}
// Stack.h
#include <iostream>
#include <assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;
void STInit(ST* ps, int n = 4);
// Stack.cpp
#include"Stack.h"
// 缺省参数不能声明和定义同时给
void STInit(ST* ps, int n)
{assert(ps && n > 0);ps->a = (STDataType*)malloc(n * sizeof(STDataType));ps->top = 0;ps->capacity = n;
}
// test.cpp
#include"Stack.h"
int main()
{ST s1;STInit(&s1);// 确定知道要插入1000个数据,初始化时一把开好,避免扩容ST s2;STInit(&s2, 1000);return 0;
}
对于缺省参数的一个应用场景是,假如我们现在要开辟一个1000大小的栈,因为我们不可能清楚每次要开辟的栈的代销,所以初始化时只能赋予空值,然后一次次判断再扩容知道满足1000大小的需求,但这样的消耗是比较大的;但是现在我们使用缺省参数,如果知道要开辟的大小,那么我们直接将数值传入,初始化时就能开好;如果不知道要开辟的大小,那初始化时可以使用缺省值开一个4大小的栈。可以说很大提升了效率。
• 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
如果同时出现,就会出现不知道以谁为准的情况,所以规定函数声明给缺省值。
六、函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调用就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。
#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;
}
// 2、参数个数不同
void f()
{cout << "f()" << endl;
}
void f(int a)
{cout << "f(int a)" << endl;
}
// 3、参数类型顺序不同(本质就是类型不同)
void f(int a, char b)
{cout << "f(int a,char b)" << endl;
}
void f(char b, int a)
{cout << "f(char b, int a)" << endl;
}int main()
{Add(10, 20);Add(10.1, 20.2);f();f(10);f(10, 'a');f('a', 10);return 0;
}
对于C语言来说,我们在定义函数时,比如一个数的相加与两个数的相加、整形与整形的相加或者浮点型与整形的相加,这些在我们朴素认知中,这些只是是参数的类型或者个数不同,本质上应该都可以通过一个函数实现,但1是C语言中不允许同名函数的参数类型或者个数不同。因此为了实现这些基本没有差别的功能,我们不得不使用不同的名字去定义函数,调用起来也非常麻烦。
但是C++支持在同一作用域中出现形参不同或者个数不同的同名函数,这样C++函数调用就表现出了多态行为,使用更灵活,比如在上述代码中的两个Add尽管定义不同,但是使用起来就像是同一的函数。这样实际上就相当于众多相似的功能都集中到一个函数上,函数更完善、更方便。
// 下面两个函数构成重载
// f()但是调用时,会报错,存在歧义,编译器不知道调用谁void f1()
{cout << "f()" << endl;
}void f1(int a = 10)
{cout << "f(int a)" << endl;
}
但是需要注意的是像上述代码实现函数重载会出现问题,两个函数的参数不同构成函数重载,但是因为第二个函数又使用的了缺省参数,对于缺省参数来说不传参会使用缺省值,所以第二个函数可以不传递参数,但是第一个函数定义的也不同传参,所以调用f1(),会出现问题,编译器不知道调用谁。
七、引用
7.1.引用的概念和定义
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。别名本质上还是那一块空间只是换了一个称呼.比如:水浒传中李逵,宋江叫"铁牛",江湖上人称"黑旋风";林冲,外号豹子头;
使用格式:类型& 引用别名 = 引用对象;
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的<< 和 >>,这里引用也和取地址使用了同一个符号&,大家注意使用方法角度区分就可以。
7.2.引用的特性
• 引用在定义时必须初始化
• 一个变量可以有多个引用(引用只是给变量取别名,本身不开空间,不论给变量取别名还是给别名取别名,指向空间都不变)
#include<iostream>
using namespace std;
int main()
{int a = 0;// 引用:b和c是a的别名int& b = a;int& c = a;// 也可以给别名b取别名,d相当于还是a的别名int& d = b;//// 这里取地址我们看到是一样的cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}
• 引用一旦引用一个实体,再不能引用其他实体
#include<iostream>
using namespace std;
int main()
{int a = 10;// 编译报错:“ra”: 必须初始化引用//int& ra;int& b = a;int c = 20;cout << a << endl;cout << b << endl;cout << c << endl;// 这里并非让b引用c,因为C++引用不能改变指向,// 这里是一个赋值b = c;cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << a << endl;cout << b << endl;cout << c << endl;
}
7.3.引用的使用
• 引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。
• 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
• 引用返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入讲解。
void Swap(int& rx, int& ry)
{int tmp = rx;rx = ry;ry = tmp;
}void Swap(int* px, int* py)
{}
以实现交换功能为例,之前实现,我们都需要传递指针才能通过函数形参改变实参,但是现在引用是函数的别名,函数形参是引用的格式,实参直接传变量就行,这样我们不用传指针,通过引用就可以改变外面的值,同时引用不会开辟空间,我们还节省了空间。
此外对于取栈顶的STTOP过去在C语言内,我们是无法通过返回值去直接修改栈内的数据的,但是现在因为引用就是对应空间的别名,我们返回引用,我们就可以通过返回值直接修改栈内的值。
vector<int> v;
for (size_t i = 0; i < v.size(); i++)
{v[i] = i;
}
上诉的功能使我们之后修改vector内的值可以像数组那样处理。(本文仅用于说明引用的价值,不详细介绍)
• 一些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复杂的指针,但是很多同学没学过引用,导致一头雾水。
#include<iostream>
using namespace std;
typedef struct SeqList
{int a[10];int size;
}SLT;
// 一些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复
//杂的指针,但是很多同学没学过引用,导致一头雾水。
void SeqPushBack(SLT& sl, int x)
{}
typedef struct ListNode
{int val;struct ListNode* next;
}LTNode, * PNode;
// 指针变量也可以取别名,这里LTNode*& phead就是给指针变量取别名
// 这样就不需要用二级指针了,相对而言简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{PNode newnode = (PNode)malloc(sizeof(LTNode));newnode->val = x;newnode->next = NULL;if (phead == NULL){phead = newnode;}else{//...}
}
int main()
{PNode plist = NULL;ListPushBack(plist, 1);return 0;
}
7.4.const引用
• 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
int main()
{const int a = 10;//int rd = a;//int& ra = a;int b = 0;const int& rb = b;b++;//rb++;//只可以读值,不可以通过别名修改值const int& ra = a;const int& rc = 30;return 0;
}
const int a的权限是只可以使用值,不可以修改值本身,权限不能放大,因此我们以相同的权限以const int & 为a1起别名(不能使用 int &起别名,这样可以通过别名修改a,权限放大了。),对于int b 我们即可以int&起别名,也可以以const int&(只可以使用,不可以修改)的权限缩小的方式为b起别名。此外const int&也可以为常量起别名。
需要注意的是权限放大的问题只存在引用与对应对象之间,两个对象间所使用的空间不同,不存在权限相互干扰的问题。
• 不过需要注意的是类似int& rb = a*3; double d = 12.34; int& rd = d; 这样一些场景下a*3的和结果保存在一个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值,也就是时,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
• 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
需要注意的是对表达式的值的结果使用引用、以不同类型引用变量发生类型转换或者整形提升等情形下,我们都需要使用const引用。
以上图第一种类型为例,当我们对表达式的结果引用时,表达式的结果是先暂存在一个临时对象(未命名的空间)内,这个未命名空间具有常性,我们需要使用const引用。(为这块未命名空间取别名后,这块未命名空间不会立刻销毁,就相当于成了一个正常的变量。)
第二种情况,由于引用的类型与对象的类型不同,对象会发生整形提升或者类型转换,这个过程的结果同样暂时存放在临时对象,然后再对这个临时对象引用。
注:以下图的push_back为例,const引用的价值在于使用之后,我们可以往参数内放任意值(表达式、常量、变量)的别名,不需要再开辟空间传值,既方便又节省。这个在进一步学习C++后会有更深的感受。
7.5.指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
• 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间(但是实际上在底层引用与指针是一致的,引用就是指针,不过一般来说,我们更关心语法层面的效果)。

• 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
• 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。(以链表为例,删除节点之后再插入节点,需要改变指向,所以这也进一步说明引用无法替代指针。)
• sizeof中含义不同,引用结果为引用类型的大小(只是起了别名,空间还是原大小的空间),但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。(也会出现空引用和野引用的概念,但是几率很小)
八、 inline
在C语言中,我们会通过将简短常用的函数改写为宏函数来减少反复的开销。但是如下图所示,想正确使用的宏函数的难度较大,说不定什么时候就踩到坑里。因此C++中就有inline内联函数来替代。
#include<iostream>
using namespace std;
// 实现一个ADD宏函数的常见问题
//#define ADD(int a, int b) return a + b;
//#define ADD(a, b) a + b;
//#define ADD(a, b) (a + b)
// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?
// 为什么要加外面的括号?
// 为什么要加里面的括号?
int main()
{int ret = ADD(1, 2);cout << ADD(1, 2) << endl;cout << ADD(1, 2) * 5 << endl;int x = 1, y = 2;ADD(x & y, x | y); // -> (x&y+x|y)return 0;
}
• 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数(不是通过开辟栈桢来调用函数,而是类似宏函数直接在调用的地方将函数的逻辑替换过去),这样调用内联函数就不需要建立栈帧了,就可以提高效率。
• inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
之所以设计成这样是因为如果不加节制的将所有函数都展开,那么函数过大且调用频繁的函数全部展开,会出现代码膨胀的问题。
假设一个函数有大约一百条指令,但是这个函数在10000个位置调用,那么不展开是1000+100(不展开,始终开辟栈桢调用同一份指令),一但展开是10000*100条(每个地方都替换展开)。
这样代码膨胀,可执行程序就会变大,安装包会非常大,严重影响用户体验。
• C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。
• vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。
#include<iostream>
using namespace std;
inline int Add(int x, int y)
{int ret = x + y;ret += 1;ret += 1;ret += 1;return ret;
}
int main()
{// 可以通过汇编观察程序是否展开// 有call Add语句就是没有展开,没有就是展开了int ret = Add(1, 2);cout << Add(1, 2) * 5 << endl;return 0;
}
• inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。推荐将加了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()
{// 链接错误:无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z)f(10);return 0;
}
九、nullptr
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif
#endif
• C++中NULL被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,遇到重载函数时,本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。f((void*)NULL);调用会报错。
#include<iostream>
using namespace std;
void f(int x)
{cout << "f(int x)" << endl;
}
void f(int* ptr)
{cout << "f(int* ptr)" << endl;
}
int main()
{f(0);// 本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int//x),因此与程序的初衷相悖。f(NULL);f((int*)NULL);// 编译报错:error C2665: “f”: 2 个重载中没有一个可以转换所有参数类型// f((void*)NULL);f(nullptr);return 0;
}
如上图,需要补充的是C++中NULL被定义为字面常量0,因为C++在相关检查方面比C语言更加严格,与C语言中void*的指针可以给任意类型的指针不同,C++中void*无法给任意类型,同时如果是隐式类型转换,那又该转换成哪一个呢?所以总的来说NULL在C++内的使用是有不小的风险的。
• C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。(nullter替代NULL,称为空指针专用)
相关文章:

【C++】C++基础
目录 一. C关键字(C98) 二、C的第一个程序 三、命名空间 3.1.namespace的价值 3.2.namespace的定义 3.2.命名空间使用 总结:在项目当中第一、第二种方法搭配使用,第三种冲突风险非常大,仅适合练习使用。 四、C输入&输出 五、缺省…...
自动化运维工具 Ansible
Ansible 基础 Ansible 介绍 Ansible 是一个自动化运维工具,基于Python开发,集合了众多运维工具(puppet、cfengine、chef、 func、fabric)的优点,实现了批量系统配置、批量程序部署、批量运行命令等功能。 Ansible 的…...

深度学习:GAN图像生成
GAN的诞生背景 诞生: 2014年由Ian Goodfellow提出 创新性: 无监督学习:GAN 提供了一种新的方法来进行无监督学习,即不需要对训练数据进行标注就可以学习到数据的潜在分布。对抗训练:通过引入对抗机制,G…...

django基于python的房价分析可视化系统的设计与开发 h1y0i
目录 项目介绍技术栈具体实现截图Scrapy爬虫框架关键技术和使用的工具环境等的说明解决的思路开发流程爬虫核心代码展示系统设计论文书写大纲详细视频演示源码获取 项目介绍 大数据分析是现下比较热门的词汇,通过分析之后可以得到更多深入且有价值的信息。现实的科…...

Labview helper
IMAQ Advanced Setup Learn Geometric Pattern 2 VI 参数说明Curve Extraction Mode (0)指定VI如何识别图像中的曲线。如果您希望VI不对图像中对象的均匀性或图像背景做出任何假设,请将此选项设置为正常。如果您希望VI假定图像中的对象或图像背景由均匀的像素值组成…...

《安富莱嵌入式周报》第343期:雷电USB4开源示波器正式发布,卓越的模拟前端低噪便携示波器,自带100W电源的便携智能烙铁,NASA航空航天锂电池设计
周报汇总地址:嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 更新一期视频教程 【授人以渔】CMSIS-RTOS V2封装层专题视频,一期视频将常用配置和用法梳理清楚࿰…...

如何用JavaScript编写一个简单的计数器
在网页开发中,计数器是一种常见的功能,它可以帮助我们记录点击次数、显示时间等。下面我将介绍如何在HTML页面中使用JavaScript实现一个基本的计数器。如图: 1、 创建HTML结构 首先,我们需要创建一个基础的HTML结构来容纳我们的计…...

uniapp中实现评分组件,多用于购买商品后,对商品进行评价等场景
前言 uni-rate是uniapp框架中提供的一个评分组件。它可以用于用户评价、打分等场景。uni-rate组件可以根据设定的星星总数,展示用户评分的效果,用户可以通过点击星星或滑动星星的方式进行评分。同时,uni-rate组件也支持自定义星星图标、星星…...

算法笔记(三)——前缀和算法
算法笔记(三)——前缀和算法 文章目录 算法笔记(三)——前缀和算法一维前缀和二维前缀和寻找数组的中心下标除自身以外数组的乘积和为 K 的子数组和可被 K 整除的子数组连续数组矩阵区域和 前缀和算法是一种用空间换时间的算法&am…...
Nginx技术深度解析与实战应用
Nginx技术深度解析与实战应用 Nginx是一款轻量级、高性能的Web服务器、反向代理服务器及电子邮件(IMAP/POP3)代理服务器,由俄罗斯的程序设计师Igor Sysoev开发。Nginx以其内存占用少、启动迅速、高并发能力强等特性,在互联网项目…...
Maven Surefire Plugin
Maven Surefire Plugin 最新版本新特性详解 Maven Surefire Plugin 是用于运行单元测试和集成测试的重要工具,支持 JUnit、TestNG 等测试框架。插件的新版本引入了许多新特性和配置选项,这些功能提升了测试执行的性能、灵活性和并发能力。在本节中&…...

八、跳跃、闪避
一、人物跳跃功能 1、动画 设置一个bool值 条件设置为true 2、逻辑 实现跳跃,一定有IsGround;判断是否为地面,进行跳跃功能 写一个跳跃和一个条约结束方法 跳跃设置为false,结束设置为true 3、代码 public void Jump() {if…...
使用辅助分类器 GAN 进行条件图像合成
Conditional Image Synthesis with Auxiliary Classifier GANs Conditional Image Synthesis with Auxiliary Classifier GANs(简称AC-GANs)是一种用于改善生成对抗网络(GANs)进行图像合成的方法。在AC-GANs中,判别器…...
C#中的static关键字:静态成员与单例模式的实现
在C#中,static 关键字是一个非常重要的概念,它用于声明静态成员,这些成员属于类本身,而不是类的任何特定实例。使用 static 关键字可以定义静态类、静态字段、静态属性、静态方法等。此外,理解静态成员也对于实现如单例…...

【优选算法】(第八篇)
目录 串联所有单词的⼦串(hard) 题目解析 讲解算法原理 编写代码 最⼩覆盖⼦串(hard) 题目解析 讲解算法原理 编写代码 串联所有单词的⼦串(hard) 题目解析 1.题目链接:. - 力扣&#…...

告别PPT熬夜!Kimi+AIPPT一键生成PPT,效率upup!
Kimi AiPPT 一键生成PPT 还在为做PPT熬夜加班吗?还在为PPT排版抓狂吗?现在,有一个好消息要告诉所有“打工人”!Kimi和AIPPT强强联手,推出了一键生成PPT功能,让你告别PPT制作的痛苦! 以前做…...

大语言模型在构建UNSPSC 分类数据中的应用
UNSPSC 是联合国标准产品和服务代码。UNSPSC由联合国开发计划署(UNDP)和Dun & Bradstreet公司(D & B)于1998年联合制定,自2003年以来一直由GS1 US管理。GS1 US 将在 2024 年底前将 UNSPSC 的管理权移交给 UNDP…...

C++初阶:STL详解(十)——priority_queue的介绍,使用以及模拟实现
✨✨小新课堂开课了,欢迎欢迎~✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:C:由浅入深篇 小新的主页:编程版小新-CSDN博客 一.priority_queue的介绍 优先级队列被实现…...
Qt | Linux+QFileSystemWatcher文件夹和文件监视(例如监视U盘挂载目录)
点击上方"蓝字"关注我们 01、QFileSystemWatcher >>> QFileSystemWatcher 是 Qt 提供的一个类,用于监视文件和目录的变化。它允许应用程序监控一个或多个文件和目录,并在这些文件或目录内容发生变化时收到通知。这使得 Qt 应用程序能够动态响应文件系统的…...

【Linux进程间通信】Linux匿名管道详解:构建进程间通信的隐形桥梁
📝个人主页🌹:Eternity._ ⏩收录专栏⏪:Linux “ 登神长阶 ” 🌹🌹期待您的关注 🌹🌹 ❀Linux进程间通信 📒1. 进程间通信介绍📚2. 什么是管道📜3…...

stm32G473的flash模式是单bank还是双bank?
今天突然有人stm32G473的flash模式是单bank还是双bank?由于时间太久,我真忘记了。搜搜发现,还真有人和我一样。见下面的链接:https://shequ.stmicroelectronics.cn/forum.php?modviewthread&tid644563 根据STM32G4系列参考手…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
一、前言说明 在2011版本的gb28181协议中,拉取视频流只要求udp方式,从2016开始要求新增支持tcp被动和tcp主动两种方式,udp理论上会丢包的,所以实际使用过程可能会出现画面花屏的情况,而tcp肯定不丢包,起码…...

Redis相关知识总结(缓存雪崩,缓存穿透,缓存击穿,Redis实现分布式锁,如何保持数据库和缓存一致)
文章目录 1.什么是Redis?2.为什么要使用redis作为mysql的缓存?3.什么是缓存雪崩、缓存穿透、缓存击穿?3.1缓存雪崩3.1.1 大量缓存同时过期3.1.2 Redis宕机 3.2 缓存击穿3.3 缓存穿透3.4 总结 4. 数据库和缓存如何保持一致性5. Redis实现分布式…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
在Ubuntu中设置开机自动运行(sudo)指令的指南
在Ubuntu系统中,有时需要在系统启动时自动执行某些命令,特别是需要 sudo权限的指令。为了实现这一功能,可以使用多种方法,包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法,并提供…...
【Elasticsearch】Elasticsearch 在大数据生态圈的地位 实践经验
Elasticsearch 在大数据生态圈的地位 & 实践经验 1.Elasticsearch 的优势1.1 Elasticsearch 解决的核心问题1.1.1 传统方案的短板1.1.2 Elasticsearch 的解决方案 1.2 与大数据组件的对比优势1.3 关键优势技术支撑1.4 Elasticsearch 的竞品1.4.1 全文搜索领域1.4.2 日志分析…...

Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...

嵌入式学习之系统编程(九)OSI模型、TCP/IP模型、UDP协议网络相关编程(6.3)
目录 一、网络编程--OSI模型 二、网络编程--TCP/IP模型 三、网络接口 四、UDP网络相关编程及主要函数 编辑编辑 UDP的特征 socke函数 bind函数 recvfrom函数(接收函数) sendto函数(发送函数) 五、网络编程之 UDP 用…...

【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...

《信号与系统》第 6 章 信号与系统的时域和频域特性
目录 6.0 引言 6.1 傅里叶变换的模和相位表示 6.2 线性时不变系统频率响应的模和相位表示 6.2.1 线性与非线性相位 6.2.2 群时延 6.2.3 对数模和相位图 6.3 理想频率选择性滤波器的时域特性 6.4 非理想滤波器的时域和频域特性讨论 6.5 一阶与二阶连续时间系统 6.5.1 …...