一篇搞定C++基础语法与核心机制
个人专栏:《数据结构-初阶》《经典OJ题目》《C语言》《小白算法成长录》
欢迎大佬交流
本文代码已同步GitHub:GitHub - Stellen-z/DailyCode: pracetice · GitHub
一、从C语言到C++
1、为什么学习C++
C++是在C语言基础上发展出来的语言,它不仅保留了C的:
- 高性能
- 直接操作内存的能力
- 适合底层开发的特性
还增加了:
- 面向对象编程(OOP)
- 泛型编程(模板)
- 标准模板库(STL)
简单理解:
C是“工具”,C++是“工具箱 + 设计系统”
学习C++的意义是:
- 从“过程式思维” → “结构化 + 对象化思维”
- 从“写单个函数” → “设计完整程序结构”
- 为后续高阶数据结构与算法打基础
2、C和C++的区别
C语言:面向过程编程
C语言以“函数”为核心,把问题拆解为一个个步骤:
- 程序 = 函数 + 数据
- 强调“怎么做”(过程)
C++:多范式语言(面向对象为主)
C++支持:
- 面向过程(兼容C)
- 面向对象(OOP)
- 泛型编程(模板)
更强调:
“谁来做这件事”(对象)
C语言不支持:
- 没有类(class)
- 没有对象
- 没有封装 / 继承 / 多态
C++支持:
- class / struct(增强版)
- 封装(封装数据和方法)
- 继承(代码复用)
- 多态(接口统一)
3、第一个C++程序
首先明确C++兼容C绝大多数的语法,因此我们先在C++环境中编译C代码:
#include <stdio.h>
int main()
{
printf("Hello World!\n");
return 0;
}

程序正常运行!
当然,C++有自己的输入输出,肯定不会像C语言这样写
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World!" << endl;
return 0;
}

不知道各位小伙伴用C++/Java第一次写出Hello World时,和第一次用C语言写出来的Hello World有什么不同的感觉呢?
下面我们就从C++的第一个程序开始学习基础语法!
二、C++基础语法
1、namespace
我们先来看一段C语言程序:
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
int main()
{
//error C2365: “rand”: 重定义;以前的定义是“函数”
printf("%d\n", rand);
return 0;
}
直接编译错误!
报错原因是重定义,当我们想打印 rand 时,在C语言的编译环境下、
rand是一个函数,而我们在全局变量中又定义了rand,导致rand的重定义!
那么在C++中,我们该怎么避免这种情况呢?
答案就是使用 namespace--命名空间
namespace 的定义
● 定义命名空间,需要使用namespace 关键字,后面跟命名空间的名字,然后加上一对{ },{ }中即为命名空间的成员;可以定义变量、函数、类等;
● namespace 本质是定义一个域,这个域跟全局域各自独立,而不同的域可以定义同名变量,因此基本解决了命名冲突
● C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找一个变量/函数/类型出处(声明或定义)的逻辑;
因此,有了域隔离,名字冲突就解决了;
局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期。
● namespace 只能定义在全局,当然也可以嵌套定义
● 在项目工程多文件中,定义的同名 namespace 会认为是一个 namespace ,不会冲突
● C++标准库都放在一个叫 std(standard) 的命名空间中
下面我们来定义上述的例子
//1.命名空间定义
#include <stdio.h>
#include <stdlib.h>
namespace stn
{
//定义变量
int rand = 20;
//定义函数
int Add(int x, int y)
{
return x + y;
}
}
int main()
{
printf("%p\n", rand);//默认访问rand函数指针
printf("%d\n", stn::rand);//访问stn命名空间中的rand
return 0;
}

//2.命名空间的嵌套
#include <stdio.h>
#include <stdlib.h>
namespace mn
{
//pq命名空间
namespace pq
{
int rand = 1;
int Add(int x, int y)
{
return x + y;
}
}
//xy命名空间
namespace xy
{
int rand = 1;
int Add(int x, int y)
{
return x + y;
}
}
}
int main()
{
printf("%d\n", mn::pq::rand);
printf("%d\n", mn::xy::rand);
printf("%d\n", mn::pq::Add(1, 2));
printf("%d\n", mn::xy::Add(1, 2));
return 0;
}

//多文件可以重复定义同名的namespace,他们会默认合并到一起
//Stack.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
namespace stn
{
// 支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
// 初始化栈
void StackInit(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
}
//Stack.cpp
#include "Stack.h"
namespace stn
{
// 初始化栈
void StackInit(Stack* ps)
{
assert(ps);
ps->_a = NULL;
ps->_capacity = ps->_top = 0;
}
// 入栈
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->_capacity == ps->_top)
{
int newcapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
STDataType* tmp = (STDataType*)realloc(ps->_a, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc failed!\n");
exit(1);
}
ps->_a = tmp;
ps->_capacity = newcapacity;
}
ps->_a[ps->_top++] = data;
}
// 出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->_top > 0);
ps->_top--;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->_top > 0);
return ps->_a[ps->_top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps)
{
assert(ps);
return ps->_top;
}
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->_top == 0;
}
// 销毁栈
void StackDestroy(Stack* ps)
{
free(ps->_a);
ps->_a = NULL;
ps->_capacity = ps->_top = 0;
}
}
//Test.cpp
#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);
STPush(&st1, 3);
printf("%d\n", sizeof(st1));//固定10个int类型,共44字节
//调用stn的栈
stn::Stack st2;
printf("%d\n", sizeof(st2));//一个x64环境下指针,两个int类型,共16字节
stn::StackInit(&st2);
stn::StackPush(&st2, 1);
stn::StackPush(&st2, 2);
stn::StackPush(&st2, 3);
}

namespace 的使用
当编译器在查找一个变量的声明/定义时,默认只会在局部或者全局查找,不会到命名空间中查找;
因此下面的程序会报错
#include <stdio.h>
namespace stn
{
int a = 0;
int b = 1;
}
int main()
{
//error C2065: “a”: 未声明的标识符
printf("%d\n", a);
return 0;
}
要使用命名空间中定义和变量、函数,有三种方式:
a、指定命名空间,项目中推荐这种方式
int main()
{
printf("%d\n", stn::a);
return 0;
}
b、using将命名空间中某个成员展开,项目中经常访问且不存在冲突的成员推荐这种方式
using stn::a;
int main()
{
printf("%d\n", a);
printf("%d\n", stn::b);
return 0;
}
c、展开命名空间全部成员,项目不推荐,冲突风险很大,日常练习可以使用
using namespace stn;
int main()
{
printf("%d\n", a);
printf("%d\n", b);
return 0;
}
2、输入输出
在C语言中,包含输入输出的头文件是<stdio.h>
而在C++中,包含输入输出的头文件是<iostream>
<iostream>是 Input Output Stream的缩写,是标准的输入,输出流库,定义了标准的输入,输出对象;
而在前面我们说过,std这个命名空间里面包含cin 和 cout
std::cin 是 istream类的对象,主要面向的是窄字符(narrow characters)的标准输入流,那么宽字符对应的就是wcin;
std::cout 是 ostream 类的对象,主要面向窄字符的标准输出,那么宽字符对应的就是wcout;
<<是流插入运算符,>>是流提取运算符
std::endl 是一个函数,流插入输出时,相当于一个换行符加刷新缓冲区;
使用C++输入输出更方便,不需要printf/scanf那样需要手动指定格式,C++的输入输出可以自动识别变量类型
cout/cin/endl等都属于C++标准库,C++标准库都放在std的命名空间中,因此要通过命名空间来使用
因此,在日常练习中我们可以 using namespace std,实际项目开发中不能直接展开

由于C++兼容C大部分语法,因此在编译时,会一并编译C语言的代码
这就导致有时C++的效率会降低
比如当需要处理大量输入输出时,为了提高效率,我们通常会加上下面这三行代码
#include <iostream>
int main()
{
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
return 0;
}
三行代码的作用:
-
std::ios_base::sync_with_stdio(false);
取消 C++ 标准流(cin/cout)与 C 标准流(stdio)之间的同步;默认情况下两者同步,以保证混用时的顺序正确,但会带来额外开销。关闭同步后,cin/cout效率大幅提升,但不能再混用printf/scanf,否则会导致未定义行为。 -
std::cin.tie(nullptr);
解除cin与cout的绑定。默认情况下,每次执行cin前会自动刷新cout,解除绑定可减少不必要的刷新开销。 -
std::cout.tie(nullptr);
解除cout与其他流的绑定(通常无额外绑定,但保持写法对称或防止未来修改)。
三、函数增强
1、缺省参数
缺省参数的定义:允许在函数声明时为参数指定默认值;调用时若省略该实参,则自动使用默认值。
缺省分为全缺省、半缺省;
全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值;
注:C++规定半缺省参数必须从右向左依次连续缺省,不能间隔跳跃给缺省值
函数定义和声明分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值
我们来看下面的例子就能明白
a、缺省参数的使用
#include <iostream>
using namespace std;
void fun(int a = 1)
{
cout << a << endl;
}
int main()
{
fun(); //没有传参时,使用参数的默认值
fun(10); //传参时,使用指定的实参
}

b、缺省参数的分类
#include <iostream>
using namespace std;
//全缺省
void Func1(int a = 1,int b = 2,int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
//半缺省
void Func2(int a, int b = 2, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
Func1(); //1 2 3
Func1(10); //10 2 3
Func1(10,20); //10 20 3
Func1(10,20,30); //10 20 30
Func2(100); //100 2 3
Func2(100,200); //100 200 3
Func2(100,200,300); //100 200 300
}

2、函数重载
C++支持在同一作用域中出现同名函数,但是要求这些同名函数的形参不同:可以是参数个数不同或者是类型不同!
注:C语言是不支持同一作用域出现同名函数的
我们通过代码来观察
#include <iostream>
using namespace std;
namespace stn
{
//1.参数类型不同
int Add(int x, int y)
{
cout << "Add(int x, int y)" << endl;
return x + y;
}
double Add(double x, double y)
{
cout << "Add(double x, double y)" << endl;
return x + y;
}
//2.参数个数不同
void func(int a)
{
cout << "func(int a)" << endl;
}
void func(int a, int b)
{
cout << "func(int a,int b)" << 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()
{
stn::Add(1, 2);
stn::Add(1.1, 2.2);
stn::func(1);
stn::func(1,2);
stn::f(1, 'x');
stn::f('x', 1);
return 0;
}

既然参数类型、参数个数、参数顺序都会构成函数重载,那么函数返回值是否会构成重载呢?
我们来试一下
#include <iostream>
using namespace std;
int f()
{
cout << "f()" << endl;
return 1;
}
//error C2556: “void f(void)”: 重载函数与“int f(void)”只是在返回类型上不同
void f()
{
cout << "f()" << endl;
}
int main()
{
f();
f();
return 0;
}
编译失败,编译器在调用时不知道到底调用哪一个函数!
最后我们来看下面这组代码
#include <iostream>
using namespace std;
void f()
{
cout << "f()" << endl;
}
void f(int a = 10)
{
cout << "f(int a = 10)" << endl;
}
int main()
{
f();//error C2668: “f”: 对重载函数的调用不明确
f(10);
return 0;
}
首先来看,这两个函数是否构成函数重载?
答案是构成!
那为什么会报错呢?
当全缺省函数和无参函数都存在时,如果调用函数时没有进行传参,就会引发歧义,编译器不知道到底该调哪个函数!
3、inline
a、内联函数的定义
用 inline 修饰的的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就不需要建立栈帧了,提高效率!
b、内联函数的使用
下面我们来写一个简单的内联函数
#include <iostream>
using namespace std;
inline int Add(int x, int y)
{
int ret = x + y;
ret += 1;
return ret;
}
int main()
{
int ret = Add(1, 2);
cout << ret << endl;
return 0;
}
通过调试来观察内联函数是否展开

发现框选部分并没有call/ret指令,即没有通过call指令进行调用,直接将代码复制过来!
最后要注意,inline不建议声明和定义分离到两个文件,分离会导致连接错误;
因为inline被展开,没有函数地址,链接时会出现报错!
c、内联函数与宏函数的区别
C++设计内联函数的目的就是为了替代C语言的宏函数
C语言的宏函数也会在预处理时替换展开,但是宏函数很复杂,并且由于直接替换,不方便调试;
下面我们来简单实现一个加法宏函数
//请问哪个宏函数是正确的?
#define Add(int a,int b) return a + b;
#define Add(a,b) return a + b;
#define Add(a,b) (a + b)
答案是上述三个均是错误的宏函数
宏函数的本质是进行替换;
首先来看第一个
//#define Add(int a,int b) return a + b;
int ret = Add(1, 2);
//展开之后变成 int ret = return 1 + 2;;
//1.return不能出现在赋值表达式
//2.末尾有两个分号
接着来看第二个
//#define Add(a,b) a + b;
int ret = Add(1, 2);
//展开之后变成 int ret = 1 + 2;;
//此时影响不大,结果正常
cout << Add(1, 2) * 3 << endl;
//这样展开之后呢?
//cout << 1 + 2 * 3 ; << endl;
//显然1.没有括号 2.分号多余
继续来看第三个
//#define Add(a,b) (a + b)
cout << Add(1, 2) << endl; // -> cout << (1 + 2) << endl;
//此时没有问题
cout << Add(1 & 2, 1 | 2) << endl; // -> cout << (1 & 2 + 1 | 2) << endl;
//显然错误,+ 的运算符优先级高于 & ,导致计算顺序错误
最后来看正确写法
#define Add(a,b) ((a) + (b));
四、引用
1、引用的概念和定义
引用的基本概念
引用是C++中一种特殊的变量类型,本质上是已存在变量的别名;
通过引用可以直接操作原变量,无需拷贝数据;
引用在定义时必须初始化,且一旦绑定到某个变量后无法更改指向。
引用的定义语法
引用的定义方式为在变量类型后加 & 符号:
- 类型匹配:引用必须与原变量类型一致(
const引用除外)。 - 必须初始化:定义时需直接绑定到一个已存在的变量。
- 无独立内存:引用不占用额外存储空间,仅作为别名存在。
我们尝试来使用引用
#include <iostream>
using namespace std;
int main()
{
int a = 1;
int& b = a;
int& c = a;
int& d = a;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}

2、const引用
a、引用const对象必须要用const来引用;const引用也可以引用普通对象,那么就会造成权限缩小的问题;
#include <iostream>
using namespace std;
int main()
{
const int a = 1;
//权限放大:error C2440: “初始化”: 无法从“const int”转换为“int &”
//int& ra = a;
//正确引用
const int& ra = a;
//error C3892: “ra”: 不能给常量赋值
//ra++;
//权限缩小
int b = 10;
const int& rb = b;
//error C3892: “rb”: 不能给常量赋值
//rb++;
return 0;
}
b、临时对象是指编译器需要一个空间暂存表达式的求职结果时,临时创建的一个未命名对象;
而在类型转换中就会产生临时对象,而临时对象具有常性,此时就需要使用常引用!
#include <iostream>
using namespace std;
int main()
{
int a = 10;
//常量值用常引用
const int& x = 20;
//error C2440: “初始化”: 无法从“int”转换为“int &”
//int& ret = a * 2;
//临时对象用常引用
const int& ret = a * 2;
double d = 11.22;
//error C2440: “初始化”: 无法从“double”转换为“int &”
//int& rd = d;
//类型转换会产生临时对象,要用常引用
const int& rd = d;
return 0;
}
3、指针和引用
引用与指针的区别
- 初始化要求:引用必须初始化,指针可以为空(
nullptr) - 语法概念上:引用是一个变量的别名,不开空间,指针是存储一个变量的地址,要开空间
- 可修改性:引用绑定后不可更改,指针可以重新指向其他地址
- 访问语法:引用直接使用变量名操作,指针需解引用(
*ptr) - 大小上:sizoef中引用的大小取决于引用类型的大小,但指针始终是地址空间所占字节个数
- 安全性:引用不存在“空引用”问题,指针可能悬空
五、空指针
1、NULL
在C语言中,NULL实际上是一个宏;
在stddef.h文件中可以看到下面代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
C++中NULL被定义成0,C语言中是一个类型为 void *、值为零的空指针常量;
我们来看下面代码
#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 x)
f((int*)0);
f((int*)NULL);//调用f(int* ptr)
//error C2665: “f”: 没有重载函数可以转换所有参数类型
//f((void*)NULL);
return 0;
}
会发现即使传入NULL,本想调用指针版本的,却仍调用整数版本!导致出现很多歧义;
最后由于C++禁止void*隐式类型转换为其他指针类型,因此会报错
2、nullptr
在C++中,为了避免函数调用歧义的问题,引入了新的关键字 nullptr;
nullptr可以转换成任意其他类型的指针;
因此,使用nullptr可以避免指针类型转换的问题;
nullptr只能隐式的转换成指针类型,而不能被转换成整数类型
如果觉得有帮助,可以关注 GitHub 项目持续更新:GitHub - Stellen-z/DailyCode: pracetice · GitHub
更多推荐

所有评论(0)