1.C++的第⼀个程序
C++兼容C语⾔绝⼤多数的语法,所以C语⾔实现的hello world依旧可以运⾏,C++中需要把定义⽂件代码后缀改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译,linux下要⽤g++编译,不再是gcc
当然C++有⼀套⾃⼰的输⼊输出,严格说C++版本的hello world应该是这样写的。
2.命名空间
2.1namespace的价值
在C/C++中,变量、函数和后⾯要学到的类都是⼤量存在的,这些变量、函数和类的名称将都存在于全局作⽤域中,可能会导致很多冲突。使⽤命名空间的⽬的是对标识符的名称进⾏本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
c语⾔项⽬类似下⾯程序这样的命名冲突是普遍存在的问题,C++引⼊namespace就是为了更好的解决这样的问题

2.2namespace的定义
-定义命名空间,需要使⽤到namespace关键字,后⾯跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
-namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域可以定义同名变量,所以下⾯的rand不在冲突了。
-C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期,命名空间域和类域不影响变量⽣命周期。
-namespace只能定义在全局,当然他还可以嵌套定义。
-项⽬⼯程中多⽂件中定义的同名namespace会认为是⼀个namespace,不会冲突。
-C++标准库都放在⼀个叫std(standard)的命名空间中。
1.正常命名空间定义
2.嵌套命名空间定义
#include <stdlib.h>#include <stdio.h>
namespace zs
{
namespace ls
{
int rand = 10;
int ADD(int a, int b)
{
return a + b;
}
}
namespace ww
{
int ADD(int a, int b)
{
return (a + b)*10;
}
}
}
int main()
{
printf("%d\n", zs::ls::rand);
printf("%d\n", zs::ls::ADD(1, 2));
printf("%d\n", zs::ww::ADD(1, 2));
return 0;
}
运行结果:
3.多⽂件中可以定义同名namespace,他们会默认合并到⼀起,就像同⼀个namespace⼀样
2.3命名空间使用
编译查找⼀个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间⾥⾯去查找。所以下⾯程序会编译报错。所以我们要使⽤命名空间中定义的变量/函数,有三种⽅式:
-1.指定命名空间访问,项⽬中推荐这种⽅式。
-2.using将命名空间中某个成员展开,项⽬中经常访问的不存在冲突的成员推荐这种⽅式。
-3.展开命名空间中全部成员,项⽬不推荐,冲突⻛险很⼤,⽇常⼩练习程序为了⽅便推荐使⽤。
#include <stdlib.h>
#include <stdio.h>namespace N
{
int a = 10;
int b = 0;
}//未指明命名空间
int main()
{
printf("%d\n", a);//报错未定义变量名
return 0;
}//1指定命名空间访问
int main()
{
printf("%d\n", N::a);
return 0;
}
//2using将某个成员变量展开
using N::a;
int main()
{
printf("%d\n", a);
return 0;
}
//3展开命名空间全部成员
using namespace N;
int mian()
{
printf("%d\n", b);
return 0;
}
3.C++输⼊&输出
-<iostream> 是 Input Output Stream 的缩写,是标准的输⼊、输出流库,定义了标准的输⼊、输出对象。
-std::cin 是 istream 类的对象,它主要⾯向窄字符(narrow characters (of type char))的标准输⼊流。
-std::cout 是 ostream 类的对象,它主要⾯向窄字符的标准输出流。
-std::endl 是⼀个函数,流插⼊输出时,相当于插⼊⼀个换⾏字符加刷新缓冲区。
-<<是流插⼊运算符,>>是流提取运算符。(C语⾔还⽤这两个运算符做位运算左移/右移)
-使⽤C++输⼊输出更⽅便,不需要像printf/scanf输⼊输出时那样,需要⼿动指定格式,C++的输⼊输出可以⾃动识别变量类型(本质是通过函数重载实现的,这个以后会讲到),其实最重要的是C++的流能更好的⽀持⾃定义类型对象的输⼊输出。
-IO流涉及类和对象,运算符重载、继承等很多⾯向对象的知识,这些知识我们还没有讲解,所以这⾥我们只能简单认识⼀下C++ IO流的⽤法,后⾯我们会有专⻔的⼀个章节来细节IO流库。
-cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使⽤⽅式去⽤他们。
-⼀般⽇常练习中我们可以using namespace std,实际项⽬开发中不建议using namespace std。
-这⾥我们没有包含<stdio.h>,也可以使⽤printf和scanf,在包含<iostream>间接包含了。vs系列编译器是这样的,其他编译器可能会报错。
#include <stdlib.h>#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
int a = 1;
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;
}
4.缺省参数
-缺省参数是声明或定义函数时为函数的参数指定⼀个缺省值。在调⽤该函数时,如果没有指定实参则采⽤该形参的缺省值,否则使⽤指定的实参,缺省参数分为全缺省和半缺省参数。(有些地⽅把缺省参数也叫默认参数)
-全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左依次连续缺省,不能间隔跳跃给缺省值。
-带缺省参数的函数调⽤,C++规定必须从左到右依次给实参,不能跳跃给实参。
-函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值。
5.函数重载
C++⽀持在同⼀作⽤域中出现同名函数,但是要求这些同名函数的形参不同,可以是参数个数不同或者类型不同。这样C++函数调⽤就表现出了多态⾏为,使⽤更灵活。C语⾔是不⽀持同⼀作⽤域中出现同名函数的。
6.引用
6.1引⽤的概念和定义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间,它和它引⽤的变量共⽤同⼀块内存空间。⽐如:⽔壶传中李逵,宋江叫"铁⽜",江湖上⼈称"⿊旋⻛";林冲,外号豹⼦头;
类型& 引⽤别名 = 引⽤对象;
注:
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<< 和 >>,这⾥引⽤也和取地址使⽤了同⼀个符号&,⼤家注意使⽤⽅法⻆度区分就可以。(吐槽⼀下,这个问题其实/挺坑的,个⼈觉得⽤更多符号反⽽更好,不容易混淆)
6.2引⽤的特性
-引⽤在定义时必须初始化
-⼀个变量可以有多个引⽤
-引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
6.3引⽤的使⽤
-引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被引⽤对象。
-引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
-引⽤返回值的场景相对⽐较复杂,我们在这⾥简单讲了⼀下场景,还有⼀些内容后续类和对象章节中会继续深⼊讲解。
-引⽤和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向,Java的引⽤可以改变指向。
-⼀些主要⽤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)//入栈
{
assert(rs.a);//满了,扩容
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)
{
printf("realloc error!\n");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
rs.a[rs.top] = x;
rs.top++;
}
//STTop函数的int&,是 ** 函数调用时,栈底层动态数组中「当前栈顶的那个具体int变量」** 的引用,这个变量存储在堆内存中,是栈实际存储的有效数据
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);cout << STTop(st) << endl;
STTop(st) += 10;
cout << STTop(st) << endl;
}运行结果:
#include <iostream>
#include <assert.h>
using namespace std;
typedef struct SeqList
{
int a[10];
int size;
}SLT;
void SeqPushBack(SLT& sl, int x);typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode,*PNode;
// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序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;
}
6.4const引⽤
-可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。简单来说,就是引用的读写权限不能比原对象的权限更大。
-不需要注意的是类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是时,rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
-所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象,C++中把这个未命名对象叫做临时对象
using namespace std;int main()
{
const int a = 10;
//int& ra = a;//这里会报错,引用的权限被放大
const int& ra = a;
//ra++;//报错,不能给常亮赋值,const引用只能读,不能写int b = 20;
const int& rb = b;//这里是权限的缩小
//rb++;//报错,不能给常亮赋值,const引用只能读,不能写return 0;
}
using namespace std;int main()
{
int a = 10;
//int& ra = 30;//报错1
const int& ra = 30; //正确1
//int& rb = a * 3;//报错2
const int& rb = a * 3;//正确2
double d = 1.0;
//int& rd =d;//报错3
const int& rd = d;//正确3
return 0;
}
报错1:报错2:报错3:三处的报错的本质原因如下
这三个报错的本质原因完全相同 :都是普通引用试图绑定
const的临时对象,导致了权限放大。只是产生临时对象的具体场景不同(字面量、表达式、类型转换)而已。正确1:正确2:正确3:三处正确的原因如下
总结
- 加
const正确的核心原因
- 权限匹配:
const引用的只读属性和临时对象的const属性一致,无权限放大;- 生命周期延长:
const引用延长了临时对象的生命周期,避免悬空引用。
const引用的核心特点
- 只读性:通过
const引用无法修改指向的值;- 绑定灵活性:可绑定左值、右值、临时对象、
const对象(普通引用仅能绑定可修改左值);- 权限规则:仅支持权限缩小 / 匹配,禁止权限放大;
- 生命周期:绑定临时对象时会延长其生命周期。
6.5指针和引⽤的关系
-C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
-语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
-引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
-引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
-引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
-sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
-指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。
7.inline
1. 什么是
inline函数?
inline是 C++ 里的一个关键字,用它修饰的函数叫内联函数。
- 普通函数调用时,程序会跳转到函数地址,建立栈帧执行,执行完再返回,这个过程有开销。
inline函数在编译时,编译器会尝试把函数体直接 "复制粘贴" 到调用它的地方,不需要建立栈帧,以此来提高运行效率。简单比喻:
- 普通函数就像去一家连锁快餐店(函数地址),每次去都要排队点餐(建立栈帧)。
inline函数就像把快餐店的厨房(函数体)直接搬到你家(调用处),不用出门排队,省了时间开销。2.
inline只是编译器的 "建议"你写了
inline,编译器不一定会照做,它会自己判断:
- 适合展开的场景:函数体非常短小(比如只有几行)、被频繁调用(比如循环里的小函数)。
- 会被忽略的场景:函数体代码多、是递归函数、或者编译器认为展开后代码膨胀带来的坏处超过了性能收益。
这是因为 C++ 标准没有强制要求编译器必须展开
inline函数,所以它更像一个 "请求" 而非 "命令"。3.
inline和 C 语言宏函数的区别C 语言里的宏函数也是在预处理时直接替换展开,但有很多缺点:
- 宏是文本替换,容易因为优先级、类型问题出错,而且无法调试。
inline函数是真正的函数,有类型检查、可以调试,同时保留了宏 "无调用开销" 的优点。- 所以 C++ 设计
inline的主要目的,就是为了替代 C 语言里容易出错的宏函数。4. 注意事项
- 调试模式默认不展开 :比如在 VS 编译器的 Debug 模式下,为了方便调试,默认不会展开
inline函数。如果需要展开,要手动修改编译器设置。- 不要分离声明和定义 :
inline函数的声明和定义最好放在同一个头文件里。如果分开写在.h和.cpp文件中,会因为编译器展开后没有留下函数地址,导致链接错误。- 适合场景 :只推荐用在 "频繁调用的短小函数" 上,不要给大函数或递归函数加
inline,因为编译器会直接忽略。
8.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);调⽤会报错。
--C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它可以转换成任意其他类型的指针类型。使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。
- NULL :是一个宏定义,在 C++ 中通常被定义为
0(字面常量),在 C 中则可能定义为(void*)0。由于它本质是整数,在函数重载场景中会导致二义性。例如调用f(NULL)时,编译器会优先匹配f(int)而非预期的f(int*)。- nullptr :是 C++11 引入的关键字,属于一种特殊的指针类型字面量。它只能隐式转换为任意指针类型,不能转换为整数类型,因此可以精准匹配指针参数的函数重载。


















