第一讲 | 解锁C++编程能力:基础语法解析
C++入门基础
- 一、C++的第一个程序
- 二、命名空间
- 三、C++输入&输出
- 四、缺省参数/默认参数
- 五、函数重载
- 六、引用
- 1.引用的特性
- 2.引用的使用
- 引用做返回值场景
- 3.const引用
- 只有指针和引用涉及权限放大、缩小的问题,普通变量没有
- 4.指针和引用的关系
- 七、inline
- 八、nullptr
一、C++的第一个程序
C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行,C++中需要把定义文件代码后缀改为.cpp,vs编译器看到是.cpp就会调用C++编译器编译(.cpp调用C++编译器编译和.c调用C编译器编译只适用于vs编译器),linux下要用g++编译,不再是gcc。
//test.cpp
#include <stdio.h>
int main()
{printf("hello world!\n");return 0;
}
当然C++有一套自己的输入输出,严格说C++版本的hello world应该是这样写的。
//test.cpp
#include <iostream>
using namespace std;
int main()
{cout << "hello world!\n" << endl;return 0;
}
二、命名空间
- 命名空间的定义:需要用到关键字
namespace,后面加上命名空间的名字,一般开发中是用项目名字做命名空间名,然后加上一对{}即可,{}里面是命名空间的成员,命名空间中可以定义函数/类型/变量等。
namespace的价值:
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。
C++引入namespace就是为了更好的解决C语言项目类似下面程序这样的命名冲突的问题。
//test.cpp
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{printf("%d\n", rand);return 0;
}

报错原因:全局变量rand和头文件<stdlib.h>中的rand()函数命名冲突
-
namespace本质:定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量、函数、类,所以下面的rand不在冲突了。 -
C++中的域:函数局部域,全局域,命名空间域,类域。域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑,也就是说,所有域都影响编译器查找规则。所以有了域隔离,名字冲突就解决了。函数局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
编译器编译查找逻辑/规则前提:编译中任一变量/函数都要能找到它们的声明和定义,说明出处(在哪一个域),否则编译器会报错。
编译器编译时语法查找一个变量/函数/类型出处(声明或定义)的查找逻辑/规则:
- 先局部再全局
- 指定域查找
-
namespace只能定义在全局,当然他还可以嵌套定义。 -
项目工程中多文件中定义的同名
namespace会认为是一个namespace,不会冲突。 -
为了防止发生命名冲突,C++标准库都放在一个叫
std(standard)的命名空间中。 -
编译查找一个变量的声明/定义时,默认只会在函数局部或者全局查找(先局部再全局),不会到命名空间里面去查找。所以下面程序会编译报错。所以我们要使用命名空间中定义的变量/函数,有三种方式:
• 指定命名空间访问,项目中推荐这种方式。(费劲,安全)
•using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐这种方式。(折中方法)
•using展开命名空间中全部成员,也就是说不需要指定命名空间了,相当于把命名空间里的全部成员扔到全局里,或者理解为先到函数局部找,再到全局找,最后到命名空间里找。这时候若全局域里定义了一个同名变量/函数/类型,会有命名冲突的风险。项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用。(不费劲,不安全)
域作用限定符/作用域操作符
::
- 左边什么都不加,找全局
- 命名空间名字
::命名空间中的成员
展开命名空间 ≠ 展开头文件
- 展开命名空间:影响编译器编译查找规则会去命名空间里找
- 展开头文件:将头文件里的内容copy过来。例如在源文件中包含头文件"xxxx.h",头文件里的内容会在这个源文件中展开。
#include <stdio.h>
//不同的域可以定义同名变量
int a = 0;
int main()
{int a = 1;//编译器编译查找规则,先局部再全局printf("%d\n", a);//::左边什么都不加,找全局printf("%d\n", ::a);return 0;
}

#include <stdio.h>
#include <stdlib.h>
// 1. 命名空间的定义,不同的域可以定义同名变量
namespace project
{//命名空间中可以定义变量/函数/类型int rand = 10;int ADD(int x, int y){return x + y;}struct Node {int data;struct Node* next;};
}
int main()
{//1.先局部再全局//默认访问的是全局的rand函数指针/地址printf("%p\n", rand);//2.指定域查找//指定project命名空间中的randprintf("%d\n", project::rand);return 0;
}

#include <stdio.h>
// 2. 命名空间可以嵌套定义
namespace pj
{int rand = 1;int ADD(int left, int right){return left + right;}namespace pj1{int rand = 2;int ADD(int left, int right){//若是这个函数里定义了变量,这个变量是局部变量,命名空间域影响的是这个函数, 不影响这个函数里的变量return (left + right) / 10;}}namespace pj2{int rand = 3;int ADD(int left, int right){return (left + right) * 10;}}
}
int main()
{printf("%d\n", pj::rand);printf("%d\n", pj::ADD(1, 2));printf("%d\n", pj::pj1::rand);printf("%d\n", pj::pj1::ADD(4, 5));printf("%d\n", pj::pj2::rand);printf("%d\n", pj::pj2::ADD(4, 5));return 0;
}

// 3. 项目工程中多文件中定义的同名namespace会认为是一个namespace,不会冲突。// Stack.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
namespace bit
{typedef int STDataType;typedef struct Stack{STDataType* a;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 bit
{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 bit
{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 bit
{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));// 调⽤bit namespace的bit::ST st2;printf("%d\n", sizeof(st2));bit::STInit(&st2, 10);bit::STPush(&st2, 1);bit::STPush(&st2, 2);return 0;
}
// 4. 默认只会在函数局部或者全局查找(先局部再全局),不会到命名空间里面去查找
#include <stdio.h>
namespace N
{int a = 1;int b = 2;
}
int main()
{//编译报错:C2065“a”: 未声明的标识符printf("%d\n", a);return 0;
}
// 5. 指定命名空间访问
#include <stdio.h>
namespace N
{int a = 1;int b = 2;
}
int main()
{printf("%d\n", N::a);return 0;
}

// 6. using将命名空间中某个成员展开
#include <stdio.h>
namespace N
{int a = 1;int b = 2;
}
using N::b;
int main()
{printf("%d\n", N::a);printf("%d\n", b);return 0;
}
// 7. 展开命名空间中全部成员
#include <stdio.h>
namespace N
{int a = 1;int b = 2;
}
using namespace N;
int main()
{printf("%d\n", a);printf("%d\n", b);return 0;
}
三、C++输入&输出
-
<iostream>是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。 -
std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流,本质是转成字符串输出。cin、cout分别支持连续的输入、输出。
-
std::endl是一个函数 end line,流插入输出时,相当于插入一个换行字符’\n’加刷新缓冲区。 -
<<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)流插入的意义:对象流到流对象cout里。 -
使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动指定格式,C++的输入输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的支持自定义类型对象的输入输出(C语言里printf/scanf支持输入输出内置类型对象,但不支持输入输出自定义类型对象。C++里cin/cout支持输入输出任意类型的对象)。
-
IO流涉及类和对象,运算符重载、继承等很多面向对象的知识,这些知识我们还没有讲解,所以这里我们只能简单认识一下C++ IO流的用法,后面我们会有专门的一个章节来细讲IO流库。( IO流:自动识别变量的类型)
-
cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式(3种)去用他们。 -
一般日常练习中我们可以
using namespace std,实际项目开发中不建议using namespace std。 -
这里我们没有包含
<stdio.h>,也可以使用printf和scanf,在包含<iostream>间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
#include <iostream>
int main()
{std::cout << "hello world!\n";int a = 0;double b = 0.1;char c = 'x';std::cout << a << std::endl;std::cout << b << std::endl;std::cout << c << std::endl;return 0;
}

#include <iostream>
using namespace std;
int main()
{int a = 0;double b = 0.1;char c = 'x';//endl << 后面可以一直加东西cout << a << " " << b << " " << c << endl << a << endl << b;//可以自动识别变量的类型cin >> a;cin >> b >> c;cout << a << endl << " " << b << endl << c << endl;return 0;
}
多次使用cin、cout,效率比C里的输入输出低,以下写法可以提高C++IO效率:
#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++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
//反例
void Func1(int a, int b = 10, int c)
{//...
}
void Func2(int a = 10, int b, int c)
{//...
}
-
调用带缺省参数的函数,C++规定必须从左到右依次给实参,不能跳跃给实参。
-
函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
#include <iostream>
#include <assert.h>
using namespace std;
void Func(int a = 0)
{cout << a << endl;
}
int main()
{Func(); // 没有传参时,使⽤参数的默认值Func(10); // 传参时,使⽤指定的实参return 0;
}
//C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
//带缺省参数的函数调用,C++规定必须从左到右依次给实参,不能跳跃给实参。
#include <iostream>
using namespace std;
//全缺省
void Func1(int a = 10, int b = 20, double c = 2.3)
{cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl << endl;
}
//半缺省
void Func2(int a, int b = 5, int c = 56)
{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;
}
五、函数重载
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 << endl;return left + right;
}
// 2、参数个数不同
void f()
{cout << "f()" << endl;
}
void f(int a)
{cout << "f(int a)" << endl << 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;
}

//返回值不同不能作为重载条件,因为调⽤时也⽆法区分
void fxx()
{}int fxx()
{return 0;
}

#include <iostream>
using namespace std;
// 下⾯两个函数构成重载
// f()但是调⽤时,会报错,存在歧义,编译器不知道调⽤谁
void f1()
{cout << "f()" << endl;
}
void f1(int a = 10)
{cout << "f(int a)" << endl;
}int main()
{f1();return 0;
}

六、引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
类型& 引用别名 = 引用对象
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的<< 和 >>,这里引用也和取地址使用了同一个符号&,大家注意使用方法角度区分就可以。
#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;++d;cout << "a = " << a << endl;cout << "b = " << b << endl;cout << "c = " << c << endl;cout << "d = " << d << endl << endl;cout << &a << endl;cout << &b << endl;cout << &c << endl;cout << &d << endl;return 0;
}


1.引用的特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体,即引用初始化后不能改变指向
#include<iostream>
using namespace std;
int main()
{int a = 10;// 编译报错:“ra”: 必须初始化引⽤//int& ra;int& b = a;int c = 20;// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向,// 这⾥是⼀个赋值b = c;cout << &a << endl;cout << &b << endl;cout << &c << endl;return 0;
}

2.引用的使用
- 引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用别名时同时改变引用对象。
- 引用传参跟指针传参功能是类似的,引用传参相对更方便⼀些。
- 引用做返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入讲解。
- 引用和指针在实践中相辅相成,功能有重叠性(例如,形参的改变影响实参),但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。
- 一些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复杂的指针。
//引用和指针在大部分功能上是有重叠的部分的,例如,形参的改变可以影响实参
//引用,形参就是实参的别名
#include <iostream>
using namespace std;
void Swap(int& rx, int& ry)
{int tmp = rx;rx = ry;ry = tmp;
}
void Swap(int* px, int* py)
{int tmp = *px;*px = *py;*py = tmp;
}
int main()
{int x = 0, y = 1;cout << x << " " << y << endl << endl;Swap(x, y);cout << x << " " << y << endl;x = 0, y = 1;Swap(&x, &y);cout << x << " " << y << endl;return 0;
}

//使用引用的基本场景
#include<iostream>
using namespace std;
typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{rs.a = (STDataType*)malloc(n * sizeof(STDataType));rs.top = 0;rs.capacity = n;
}
int main()
{ST st;STInit(st);return 0;
}
//使用引用的基本场景
//大部分书籍上还有这样写的
//单链表的尾插函数参数是二级指针,用引用的话会降低理解难度,避开二级指针
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode
{int val;struct ListNode* next;
}LTNode, *PNode;//相当于
//typedef struct ListNode LTNode;
//typedef struct ListNode* PNode;void ListPushBack(PNode& phead, int x)
{PNode newnode = (PNode)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->val = x;newnode->next = NULL;if (phead == NULL){phead = newnode;}else{//...}
}
int main()
{PNode plist = NULL;ListPushBack(plist, 1);ListPushBack(plist, 2);ListPushBack(plist, 3);return 0;
}
PNode是结构体指针类型struct ListNode*的别名,LTNode是结构体类型struct ListNode的别名。等价于typedef struct ListNode LTNode;、typedef struct ListNode* PNode;
有typedef,定义的是类型;没有typedef,定义的是变量,LTNode是结构体变量,PNode是结构体指针变量:
struct ListNode {int val;struct ListNode* next;
}LTNode, *PNode;
C++的引用和C里的指针是相辅相成的,前面的示例中引用替代了指针,但是以下场景引用就替代不了。
定义链表的结构时,不能去引用下一个结点,必须用指针。例如,将其中一个结点销毁了,引用的指向就改变了。其次,引用必须初始化,也没有空引用的概念,尾结点指向空,实现不了空引用。像数据结构中的链表、树就不能用引用。

引用理论上是没有空间的开销的,但是引用的底层是指针,实际上是有空间开销的,这个在以后的学习中会讲解。
引用做返回值场景
#include <iostream>
#include <assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{rs.a = (STDataType*)malloc(n * sizeof(STDataType));rs.top = 0;rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{// 满了, 扩容if (rs.top == rs.capacity){printf("扩容\n");int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}rs.a = tmp;rs.capacity = newcapacity;}rs.a[rs.top] = x;rs.top++;
}// int STTop(ST& rs)
int& STTop(ST& rs)
{assert(rs.top > 0);return rs.a[rs.top - 1];
}
int main()
{ST st;STInit(st);STPush(st, 1);STPush(st, 2);STPush(st, 3);STPush(st, 4);cout << STTop(st) << endl;STTop(st) += 10;cout << STTop(st) << endl;return 0;
}
在理解传引用返回之前先理解传值返回,传值返回编译会报错,传值返回不是将这个值进行返回,而是在这儿拷贝生成一个临时对象,用这个临时对象去做下面表达式的返回值,但是这个临时对象具有常性(也就是一个右值),和const一样,不能被修改。
传引用返回的返回对象是返回对象的别名,在这里直接就是栈中的对象,传引用返回本质就是可以修改返回对象。
总的来说,传值返回与传引用返回的区别:传值返回返回的是返回对象的拷贝,传引用返回返回的是返回对象的别名。

左值、右值都是一个表达式,可以取地址的就是左值,不能取地址的就是右值。下面的a、ptr就是一个表达式,叫做变量表达式,所以变量也是一个表达式。a、ptr、*ptr都是左值,都可以取地址。常见的字面量、常量、临时对象、匿名对象都是右值。大多情况下,左值能修改,右值不能被修改。

不是任何场景下都能用引用值返回的,例如:野引用,不过大多数不会报错,但是这会很危险。

ret就不在了,func(1, 2)就成为了野引用。
下面也一样:

func()、STTop()两个函数为什么会有这样的区别呢?
STTop()函数结束,函数栈帧销毁,返回对象不在STTop()栈帧中,因为数组这块空间是malloc出来的,数组中的数据是在堆上,所以返回它的引用没问题。
func()函数结束,函数栈帧销毁,返回对象在func()栈帧中,返回它的引用有问题。
3.const引用
可以引用一个const对象,但是必须用const引用,下面的a是const int类型。const引用也可以引用普通对象,因为对对象的访问权限在引用过程中可以缩小,但是不能放大。还可以直接const引用字面量常量。
需要注意的是类似int& rb = a*3; 、double d = 12.34; 、int& rd = d;这样一些场景下a*3的结果保存在一个临时对象中, int& rd = d 也是类似,相近的类型可以隐式类型转换,在类型转换中会产生临时对象存储中间值,也就是说,rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
临时对象:编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名的对象叫做临时对象。(4/8个字节的小值暂存在寄存器里,大一点的值在内存中找一个区域存储起来)
哪些场景会出现临时对象呢?
- 函数传值返回
- 表达式运算,如a + b,表达式结果在临时对象里
- 类型转换
int main()
{
可以引用一个const对象,但是必须用const引用。const int a = 10;// 编译报错:error C2440: “初始化”: ⽆法从“const int”转换为“int &”// 这⾥的引⽤是对a访问权限的放⼤//int& ra = a;// 这样才可以const int& ra = a;// 编译报错:error C3892: “ra”: 不能给常量赋值// const限制变量不能被修改//ra++;//a++;const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。// 这⾥的引⽤是对b访问权限的缩⼩int b = 20;const int& rb = b;// 编译报错:error C3892: “rb”: 不能给常量赋值//rb++;//rb只能读不能修改b++;//但是对b没有影响还可以直接const引用字面量常量const int& rc = 30;return 0;
}
//对a、字面量访问权限放大的示例及改进
//void func(int& x)
//{
//
//}
//改进:
void func(const int& rx)
{}
int main()
{const int a = 20;func(a);func(10);return 0;
}

//rb和rd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。
int main()
{int a = 10;//int& ra = a * 3;const int& ra = a * 3;double d = 12.34;//int& rd = d;const int& rd = d;return 0;
}
只有指针和引用涉及权限放大、缩小的问题,普通变量没有

4.指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅成,功能有重叠性,但是各有自己的特点,互相不可替代。
语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。(底层实现上引用是开空间的,重点理解语法概念,底层作为了解)
类比鱼香肉丝:
- 语法概念:名字表达
- 底层:实际做这个食物,鱼香肉丝没有鱼
引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
指针很容易出现空指针和野指针的问题,引用很少出现(不代表没有空引用、野引用),引用使用起来相对更安全一些。
七、inline
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。
inline对于编译器而言只是一个建议,为什么只是个建议呢?也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。
vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。
inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
八、nullptr
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif
#endif
指针是内存单元的编号,空指针是内存地址开始的第一个字节的编号。
C++中NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦。例如,C++里本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。C里本想通过f(NULL)调用指针版本的f(int*)函数,但是NULL被定义成((void *)0),没法匹配给整型指针类型的指针,调用都会报错。
解决办法:在C++里nullptr就可以解决上述缺陷。
C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。
#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(int)f(0);// C++里本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。f(NULL);// 强制转换也行,但就是怪f((int*)NULL);// 编译报错:error C2665: “f”: 2 个重载中没有⼀个可以转换所有参数类型// f((void*)NULL);//解决办法:nullptr,只能被隐式地转换为指针类型,而不能被转换为整数类型f(nullptr);return 0;
}

相关文章:
第一讲 | 解锁C++编程能力:基础语法解析
C入门基础 一、C的第一个程序二、命名空间三、C输入&输出四、缺省参数/默认参数五、函数重载六、引用1.引用的特性2.引用的使用引用做返回值场景 3.const引用只有指针和引用涉及权限放大、缩小的问题,普通变量没有 4.指针和引用的关系 七、inline八、nullptr 一…...
LeetCode 热题 100_划分字母区间(80_763_中等_C++)(贪心算法(求并集))
LeetCode 热题 100_划分字母区间(80_763) 题目描述:输入输出样例:题解:解题思路:思路一(贪心算法(求交集)): 代码实现代码实现(思路一(贪心算法(求…...
C++ --- 多态
1 多态的概念 多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多 态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时 多态(静态多态)主要就是我…...
HAL库中使用空闲中断+DMA接收数据,接收失败的问题
问题: 串口屏与单片机通过串口(USART1)进行通信,调试时发现问题,现象如下: 手动页面的几个文本,输入的数字不会显示出来,比如初始值为0,输入200,200会一闪而…...
【STM32实物】基于STM32的扫地机器人/小车控制系统设计
基于STM32的扫地机器人/小车控制系统设计 演示视频: 基于STM32的扫地机器人小车控制系统设计 简介:扫地机器人系统采用分层结构设计,主要包括底层硬件控制层、中间数据处理层和上层用户交互层。底层硬件控制层负责对各个硬件模块进行控制和数据采集,中间数据处理层负责对采…...
【Scrapy】Scrapy教程8——处理子链接
通过前面几篇文章,已经了解了如何去爬取网页内容并存储到数据库,但是目前只是存储了一个页面的内容,现在想要获取每篇文章链接内的文章内容,我们来看看怎么获取。 生成新请求 首先我们肯定要先拿到链接,所以第一步都获取文章标题和链接肯定少不了,然后再爬取获取到到子…...
使用pycel将Excel移植到Python
1.适用需求 有些工作可能长期适用excel来进行公式计算,当需要把工作流程转换为可视化界面时,开发人员不懂专业逻辑,手动摸索公式很大可能出错,而且费时费力 2.可用工具及缺点 pandas 方便进行数据处理,支持各种格…...
学习应用层
应用层概述 客户/服务器方式(C/S)和对等方式(P2P) 动态主机配置协议DHCP 客户/服务器方式 DHCP报文会被封装成为UDP用户数据报,DHCP服务器端口号是UDP67,用户是UDP68。 广播发送,是因为并不知道…...
Doris官网上没有的一些Fe参数了,都在源码中
一、FE配置源码 apache-doris-src\fe\fe-common\src\main\java\org\apache\doris\common\Config.java 二、BE配置源码 apache-doris-src\be\src\common\config.cpp 三、FE源码 package org.apache.doris.common;public class Config extends ConfigBase {ConfField(descri…...
蓝桥杯算法精讲:二分查找实战与变种解析
适合人群:蓝桥杯备考生 | 算法竞赛入门者 | 二分查找进阶学习者 目录 一、二分查找核心要点 1. 算法思想 2. 适用条件 3. 算法模板 二、蓝桥杯真题实战 例题1:分巧克力(蓝桥杯2017省赛) 例题2:砍竹子࿰…...
C++脚本化方案调研
1 什么是脚本化 脚本化(Scripting)是指将脚本语言嵌入到主程序(C等编译型语言)中,通过以下方式扩展程序能力: 动态逻辑控制:通过脚本实现运行时逻辑调整,无需重新编译主程序&#x…...
蓝桥杯(N皇后问题)------回溯法
题目描述 在 NN 的方格棋盘放置了 N 个皇后,使得它们不相互攻击(即任意 2 个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成 45 角的斜线上。你的任务是,对于给定的 N,求出有多少种合法的放置方法…...
再探C语言(1)
温馨提示: 学C语言就像玩《掘地求升》——你以为懂了语法就能通关? 不!编译器会用铁锤教你做人!(╯‵□′)╯︵┻━┻ 🐱Part 1:sizeofの跨平台迷惑行为 Q1. 不同环境下sizeof(int)的结果 运行环境结果&a…...
高项第十三章——项目资源管理
什么是资源管理?项目资源管理包括识别、获取和管理所需资源以成功完成项目的各个过程。 本过程关注两类资源:实物资源包括设备、材料、设施和基础设施 团队资源或人员指的是团队的人力资源 13_1 项目资源管理基础 项目团队是执行项目工作,…...
C/C++转换为字符串宏和字符串拼接宏的综合使用
本文内容参考: C/C++ 宏拼接和宏展开为字符串 - DoubleLi - 博客园 特此致谢! 1. 转换为字符串宏与字符串拼接宏 (1)转换为字符串宏 转换为字符串的宏为: #define STR(x) #x //转字符串 (2)字符串拼接宏 字符串拼接的宏为: #define CONCAT(x,y) x##y //拼接 2…...
Linux:xxx is not in the sudoers file. This incident will be reported.
报错 xxx is not in the sudoers file. This incident will be reported.解决方式 切换到root用户下操作 # 1、修改/etc/sudoers文件为可修改,默认是只读的 ls -lh /etc/sudoers -r--r----- 1 root root 4.3K Dec 1 01:45 /etc/sudoerschmod uw /etc/sudoersls…...
掌握新编程语言的秘诀:利用 AI 快速上手 Python、Go、Java 和 Rust
网罗开发 (小红书、快手、视频号同名) 大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等…...
个人常用的chrome好用插件
chrome可以说是兼容性和实用性较高的浏览器 没有复杂的ui 沉重的广告 加上各种各样的浏览器插件 现在罗列一下个人常用的几款好用的插件 1. Adblock Plus 一款免费的广告拦截器,可以拦截大部分网站上的广告推荐,还你一个干净舒服的页面 以下为b站演示…...
Redis 内存优化
Redis 内存优化 Redis性能优化可以从多个方面进行,主要包括以下几个方面: 1. 内存优化 Redis 是基于内存的数据库,优化内存使用可以提高性能并降低成本。 (1) 使用合适的数据结构 不同的数据结构占用的内存不同,选择合适的数据…...
数据库设计-笔记2
1.介绍一下MySQL 历史与发展 MySQL 最初由瑞典的 MySQL AB 公司开发,于 1995 年正式发布。2008 年,MySQL AB 公司被 Sun Microsystems 收购,之后 Sun 又被甲骨文(Oracle)公司收购,MySQL 成为 Oracle 旗下…...
【大模型】什么是循环神经网络(RNNs)
在人工智能(AI)的世界里,**循环神经网络(Recurrent Neural Networks, RNNs)**是一种非常强大的工具,特别适合处理序列数据。无论是语言、时间序列还是音乐,RNNs都能帮助我们理解和预测这些数据的…...
hexo+butterfly搭建博客网站总结篇
hexobutterfly搭建博客网站总结篇 文章目录 hexobutterfly搭建博客网站总结篇0.往期栏目1.发现的不错的butterfly博主2.待实现的功能 && 结语笔者待实现的功能结语 0.往期栏目 个人博客网站搭建_为了前进而后退,为了走直路而走弯路的博客-CSDN博客 【Node…...
损失函数理解(二)——交叉熵损失
损失函数的目的是为了定量描述不同模型(例如神经网络模型和人脑模型)的差异。 交叉熵,顾名思义,与熵有关,先把模型换成熵这么一个数值,然后用这个数值比较不同模型之间的差异。 为什么要做这一步转换&…...
基于随机森林回归预测葡萄酒质量
基于随机森林回归预测葡萄酒质量 1.作者介绍2.随机森林算法与数据集介绍2.1定义2.2核心思想2.3主要步骤2.4数据集介绍 3.算法实现3.1数据加载与探索3.2数据可视化3.3数据预处理(标准化、划分训练/测试集)3.4模型训练与优化(随机森林回归 超参…...
【Qt】QWidget属性2
🏠个人主页:Yui_ 🍑操作环境:Qt Creator 🚀所属专栏:Qt 文章目录 1. windowOpacity属性2. cursor属性2.1 自定义光标 3. font属性4.tooltip属性5. focusPolicy属性6. 总结 由于QWidget的常见属性实在太多&a…...
OpenGL ES ->乒乓缓冲,计算只用两个帧缓冲对象(Frame Buffer Object)+叠加多个滤镜作用后的Bitmap
乒乓缓冲核心思想 不使用乒乓缓冲,如果要每个滤镜作用下的绘制内容,也就是这个滤镜作用下的帧缓冲,需要创建一个Frame Buffer Object加上对应的Frame Buffer Object Texture使用乒乓缓冲,只用两个Frame Buffer Object加上对应的F…...
数据库练习2
目录 1.向heros表中新增一列信息,添加一些约束,并尝试查询一些信息 2.课堂代码练习 3.题目如下 一、单表查询 1、显示所有职工的基本信息。 2、查询所有职工所属部门的部门号,不显示重复的部门号。 3、求出所有职工的人数。 4…...
macOS Sequoia 15.3 一直弹出“xx正在访问你的屏幕”
🙅 问题描述 macOS 系统升级后(15.2或者15.3均出现过此问题),不管是截图还是开腾讯会议,只要跟捕捉屏幕有关,都一直弹出这个选项,而且所有软件我都允许访问屏幕了,这个不是询问是否…...
OpenCV图像拼接(1)自动校准之校准旋转相机的函数calibrateRotatingCamera()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 cv::detail::calibrateRotatingCamera 是OpenCV中用于校准旋转相机的函数。它特别适用于那种相机相对于一个固定的场景进行纯旋转运动的情况&…...
Conda常用命令汇总(持续更新中)
原文章:安装和使用Miniconda来管理Python环境-CSDN博客 一、Miniconda的使用 Miniconda没有GUI界面,只能通过conda命令对Python环境和软件包进行管理,所以这里主要介绍一下conda的常用命令。 1. Conda相关 (1)查询conda版本 conda --vers…...
