C++ 核心语法速过:命名空间、引用、函数重载与 nullptr 深度解析

前言

本篇文章将讲解C++中的命名空间、引用、函数重载、nullptr,话不多说,直接开始吧

命名空间

在讲解这个内容前,先看一段C语言的代码吧。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
// 编译报错:error C2365: "rand": 重定义;以前的定义是"函数"
printf("%d\n", rand);
return 0;
}

为什么会出现这种情况?在C/C++中,在模块/脚本顶层直接声明的变量、函数和类,会存在于全局作用域中,这些有可能会导致冲突,例如上面的代码,这里rand变量与stdlib.h的库中rand函数冲突了,所以才编译报错。

为此,C++中用namespace解决这个问题,使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染。

namespace的定义

namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以上面这种类似的问题就能解决了。

定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。

命名空间的使用

有三种方式:

1.指定命名空间访问,项⽬中推荐这种⽅式。

  1. using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。

3.展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。

对于using namespace std这种写法是极其不推荐的,这样写会将C++的std标准库中的所有标识符暴露在全局域中,在调试时极易造成命名冲突,并且是不建议写在.h头文件中的,因为如果写在了头文件中,那么引用了这个头文件的源文件都会被迫引入整个std命名空间的,会导致命名冲突,所以尽量不要用这种,对于要常用的引入就行了,部分不常用的可以用**std::**这个,给个代码例子

c 复制代码
#include <stdio.h>
#include <stdlib.h>
// 1. 正常的命名空间定义
namespace lem
{
// 命名空间中可以定义变量/函数/类型
int rand = 10;
int Add(int x, int y)
{
return x + y;
}
struct Node
{
struct Node* next;
int val;
};
}
int main()
{
// 这⾥默认是访问的是全局的rand函数指针
printf("%p\n", rand);
// 这⾥指定bit命名空间中的rand
printf("%d\n", lem::rand);
return 0;
}

用这段代码中的用法更为稳妥一点。

C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所以有了域隔离,名字冲突就解决了。

局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。

namespace只能定义在全局,当然它还可以嵌套定义,就是命名空间里镶嵌了一个命名空间。

项⽬⼯程中多⽂件中定义的同名namespace会认为是同一个namespace,编译器在编译和链接阶段会将同名的命名空间合并,只要你的多个文件中namespace里的成员命名不同就不会冲突。这也就解释了为什么在头文件中运用using namespace std极易产生命名冲突的问题了。

C++输入输出

是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。

std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输⼊流。

std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。

std::endl是操纵符, 底层实现是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。

<<是流插⼊运算符,>>是流提取运算符(C语⾔还⽤这两个运算符做位运算左移/右移)。

使用C++对于这方面比C更加方便,C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的)最重要的是C++的流能更好的⽀持⾃定义类型对象的输⼊输出。

缺省参数

缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数),一般是给形参缺省值。

全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。

函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。

函数重载

C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。。这个就是函数重载。

这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。

代码例子如下:

cpp 复制代码
// 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;
}

引用

引用是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。使用方法:类型&引用别名 = 引用对象;,代码例子:

cpp 复制代码
#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 << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}

可以看到abcd都是同一个地址。

引用的特性

1.引用在引用对象定义时必须初始化
2.⼀个变量可以有多个引⽤
3.引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体

引用的使用

引⽤在实践中主要是用于引⽤传参和传引⽤返回中减少拷⻉提⾼效率和通过改变引⽤对象来改变被引⽤对象。

引用传参功能与指针传参功能类似,引用传参更加方便。

C++中引用能够显著提升程序性能,主要有以下三点:

1.避免了对象的拷贝开销。(核心原因)

当函数参数按值传递(Pass by Value)时,C++ 会调用对象的拷贝构造函数,在内存中完整复制一份数据。由于引用底层是指针,在64位系统下是8个字节,若是拷贝的对象只是几个字节,那还是拷贝更加方便,若是非常复杂的对象,那么用引用更加方便。

2.底层指针实现的低开销。

在 C++ 的编译阶段,引用通常被编译器实现为指针。

当你传递一个引用时,底层传递的是该对象的内存地址(在 64 位系统上固定为 8 字节)。

传递地址的开销是固定且极小的,不会随着对象体积的增大而增加。

相比于在栈上分配大块内存并逐字节复制,传递 8 字节地址的 CPU 指令周期要少得多。

3.避免了临时对象的创建与销毁。

如果不使用引用,除了拷贝本身,还会触发对象的生命周期管理。

1.构造与析构 :按值传递会在函数调用时构造一个局部副本,函数返回时再析构它。这涉及到堆内存的分配(new/malloc)和释放(delete/free),这些系统调用的开销远大于普通的内存读写。

2.缓存友好性 :频繁的内存分配和释放会导致内存碎片,降低 CPU 缓存(Cache)的命中率。引用直接操作原对象,内存布局更紧凑,对 CPU 缓存更友好。

引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向。

引用与指针功能类似,但是还是有区别的。

语法概念上引用是已存在变量的别名,不占用内存,指针存储一个变量地址,要开空间。

引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。

引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象,⽽指针可以在不断地改变指向对象。

引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。

sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)

指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。

给段代码例子:

cpp 复制代码
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];
}
int main()
{
// 调⽤全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
STTop(st1) += 10;
cout << STTop(st1) << endl;
return 0;
}

const引用

要注意的是类似 int& rb = a3; double d = 12.34; int& rd = d; 这样⼀些场景下a3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。

临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,编译器生成的临时对象,默认被视为"只读"的,你无法直接修改它。它只是一个中间值,修改没有意义,编译器会阻止这种行为。

inline

在 C/C++ 中,inline(内联)的核心作用是向编译器提出一个"优化建议",请求将函数体直接展开到调用处,从而消除函数调用的额外开销。

那么inline有哪些作用呢?

1.消除函数调用开销(性能优化)

普通函数调用需要经历"压栈参数、保存现场、建立栈帧、指令跳转、退栈恢复"等一系列繁琐的底层操作。

inline 的作用就是让编译器把函数代码"复制粘贴"到调用的地方。既然代码合并了,就不需要跳转,也不需要建立新的栈帧,从而实现了零调用开销。这在循环中频繁调用微小函数时,性能提升非常明显。

2.替代宏

宏是纯文本替换,没有类型检查,容易踩坑(如 i++ 多次自增)。

inline 是真正的函数,有严格的类型检查和作用域,且在 Debug 模式下可以单步调试(Step Into),而宏无法调试。

并且inline对于编译器来说只是一个建议,编译器看情况用不用,函数简单并且频繁调用就会用inline,若函数复杂,编译器则不会采用,退为普通的函数调用

并且类内部定义并且没有声明的成员函数默认是 inline 的 如果你直接在 class 或 struct 的花括号 {} 内部写了函数的实现,编译器会自动将其视为 inline 函数。

inline不建议声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

宏的正确使用

c 复制代码
#define ADD(a, b) ((a) + (b))

这里要考虑到运算符优先级的问题,所以里面也要加括号,宏就是文本替换。

nullptr

C++中NULL可能被定义为字⾯常量0,或者C中被定义为⽆类型指针(void*)的常量。不论采取何种定义,在使⽤空值的指针时,都不可避免的会遇到⼀些⿇烦,本想通过f(NULL)调⽤指针版本的f(int*)函数,但是由于NULL被定义成0,调⽤了f(int x),因此与程序的初衷相悖。在C++中f((void*)NULL);调⽤会报错。

于是就有了nullptr,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。

结束语

那么本篇文章到此就结束了,若对你有所帮助,还请多多点赞关注支持一下,若有什么错误的地方或者有遗漏的,还请不吝赐教。

相关推荐
南讯股份Nascent38 分钟前
2026年CRM技术选型观察:当“数据打通“成为第一需求,电商CRM的架构价值在哪里
java·大数据·用户运营
心易行者43 分钟前
Python自动化测试7步落地法:用python在线运行省掉90%环境配置时间
java·开发语言·人工智能·python·log4j·ai编程
汉克老师1 小时前
CSP-J 初赛(以满分为目标):第二十七课 《图的遍历——BFS广度优先搜索——像“水波纹”一样,一层一层地搜索》
c++·csp-j·小学生·学c++编程
闻缺陷则喜何志丹1 小时前
【动态规划】P3609 [USACO17JAN] Hoof, Paper, Scissor G
c++·算法·动态规划·洛谷
啦啦啦~~~2221 小时前
PC端+安卓端阅读器推荐!开源本地小说阅读器软件,
android·论文阅读·windows·开源软件·福昕阅读器
霸道流氓气质1 小时前
Spring AI 技术细节:VectorStore 多库统一抽象
java·人工智能·spring
今天AI了吗1 小时前
Codex 配置自定义 AI API 完整指南:从零到一接入你的专属模型
java·人工智能·python·数据分析·embedding
汉克老师1 小时前
CSP-J 初赛(以满分为目标):第二十六课 《图的遍历——DFS深度优先搜索——从“树的先序遍历”走进真正的图世界》
c++·csp-j·小学生·学c++编程
程序猿编码2 小时前
基于GGML的C++17轻量化语音推理引擎:说话人识别与语音分析技术全解析
开发语言·c++·pytorch·深度学习·神经网络·大模型