代码的注释也是本文章的重要内容,但是注释后字体颜色太浅,所以我将 // 写成 / 方便阅读

一、C++参考文档

https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp

二、C++的第一个程序

由于C++兼容C语言的大多数语法,因此C语言代码也可在.cpp文件下运行,下面是hello world的C和C++实现,大家可以感受一下差异

#include<stdio.h>

int main()
{
	printf("hello world\n");
	return 0;
}
#include<iostream>
using namespace std;

int main()
{
	cout << "hello world" << endl;
	return 0;
}

std cout等我们都看不懂,没关系,接下来我将针对这个简单的代码,将C++的一些基础知识详细讲解

三、namespace命名空间

1.namespace的价值

我们先来看一段代码

#include<stdio.h>

int a = 0;
int a = 10;

int main()
{
	printf("%d", a);
	return 0;
}/编译报错“a”: 重定义;多次初始化
​

在多人协作的大项目中,不可避免的会产生同名变量函数等,在C语言中,同一个作用域下一个变量不允许出现第二次,C++则使用namespace来避免不同代码模块之间出现命名冲突

2.namespace的定义

namespace后面跟命名空间的名字,再加上一对{},就是命名空间的格式了。{}中即为命名空间的成员,命名空间中可以定义变量、函数、类型等

namespace test
/test就是命名空间名

{
	/命名空间中可以定义变量、函数、类型
	int a = 0;
	
	int add(int left, int right)
	{
		return left + right;
	}

	struct Node
	{
		struct Node* next;
		int val;
	};
}
  • namespace本质是定义出一个域,这个域跟全局域各自独立,不同的域可以定义同名变量
  • C++中域有函数局部域,全局域,命名空间域,类域;域影响的是编译时语法查找⼀个变量/函数/ 类型出处(声明或定义)的逻辑,所有有了域隔离,名字冲突就解决了。局部域和全局域除了会影响编译查找逻辑,还会影响变量的生命周期,命名空间域和类域不影响变量生命周期
  • namespace只能定义在全局,当然它还可以嵌套定义

讲到这,我们就能用namespace解决同名冲突问题了,就是将两个同名a放到不同的命名空间中,这里我们顺便演示一下嵌套定义,test中镶嵌了one two两个命名空间

#include<stdio.h>

namespace test
{
	/第一个a
	namespace one
	{
		int a = 0;
	}
	/第二个a
	namespace two
	{
		int a = 10;
	}
}
int main()
{
	printf("%d\n", test::one::a);
	printf("%d\n", test::two::a);
}
  • 如同指针解引用需要借用符号* ,命名空间借助运算符::访问命名空间中的标识符
  • 项目工程中多文件中定义的同名namespace会认为是⼀个namespace,不会冲突
    例如,在test.cpp文件下的fruit命名空间中加入int apple=1;在make.cpp的文件下的fruit命名空间中加入int banana=3;在最后的main函数中输出apple,banana,都是调用fruit命名空间,fruit::apple, fruit::banama

3.命名空间的使用

编译查找一个变量的声明或定义时,默认只会在局部或者全局查找,不会到命名空间里面去找,所以我们要使用命名空间的成员,有三种方法:

1. 指定命名空间访问,项目中推荐该方法
2. using将命名空间中某个成员展开,项目中经常访问的不存在冲突的成员推荐该方法
3. 展开命名空间中全部成员,项目不推荐,冲突风险很大,日常小练习程序为了方便推荐使用

#include<stdio.h>

namespace test
{
	int a = 0;
	int b = 10;
}
/1、指定命名空间访问
int main()
{
	printf("%d\n", test::a);
	return 0;
}
/2using将命名空间某个成员展开
using test::b;
int main()
{
	printf("%d\n", test::a);
	printf("%d\n", b);
	return 0;
}
/3、展开命名空间中全部成员
using namespace test;
int main()
{
	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}

C++标准库都放在⼀个叫std(standard)的命名空间中,如前面hello world程序中的cout,endl,都是std的成员,为了方便使用,所以我们将std的全部成员都展开了——using namespace std;

四、C++输入&输出

  • iostream 是Input Output Stream的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象,在cpp文件中要包含该头文件。(在C语言中的头文件通常有.h,注意区分)
  • std::cin 是istream类的对象,标准输入流(对标C的scanf)
  • std::cout是ostream类的对象,标准输出流(对标C的printf)
  • std::endl是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区(对标C的 \n)
  • <<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移)
  • cout/cin/endl等都属于C++标准库,C++标准库都放在⼀个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用他们
  • ⼀般日常练习中我们可以using namespace std,实际项目开发中不建议这么展开

以下是C和CPP输入输出的对比代码,大家好好感受一下

#include<iostream>
using namespace std;

int main()
{
	int a = 0;
	double b = 0.4;
	char c = 'x';
	scanf("%d%lf", &a, &b);
	printf("%d %lf\n", a, b);
	return 0;
}
int main()
{
	int a = 0;
	double b = 0.4;
	char c = 'x';
	
	/cpp的输入输出更方便,系统会自动识别变量的类型,因此输入输出时不需要标明数据类型
	/ 还支持自定义类型对象的输入输出
	cin >> a;
	cin >> b >> c;

	cout << a << endl;
	cout << b << " " << c << endl;
	return 0;
}

讲到这里大家对C++应该有了一个初步的认识了,让我们重新来看C++实现的hello world

#include <iostream>
/包含头文件
using namespace std;
/将std的成员全部展开
int main()
{ 
	cout << "hello world" << endl; 
	/cout输出流, <<"要输出的东西 ", endl换行和刷新缓冲区
	return 0;
}

五、缺省参数(默认参数)

  • 缺省参数是声明或定义函数时为函数的一个参数指定一个缺省值,如果觉得缺省这个词太抽象,可以直接把其理解为默认值。当调用函数时,如果没有为该参数传入实参,函数就会使用设定好的默认值。
  • 缺省参数分为全缺省和半缺省参数。全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值(不是真的一半)。C++规定半缺省参数必须从右往左依次缺省,不能间隔跳跃给缺省参数(方便编译器理解)
  • 带缺省参数的函数调用,C++规定必须从左到右依次给实参(和给缺省参数相反),不能跳跃给实参
  • 函数声明和定义分离时,缺省参数不能在函数声明和定义中同时出现,规定必须函数声明给缺省值
​
​
#include <iostream>
using namespace std;

/ 示例1: 全缺省参数
void func1(int a = 1, int b = 2, int c = 3) 
{
    cout << "a=" << a << ",b=" << b << ",c=" << c << endl;
}

/ 示例2: 半缺省参数,从右向左连续缺省
void func2(int a, int b = 2, int c = 3) 
{
    cout << "a=" << a << ",b=" << b << ",c=" << c << endl;
}

/ 函数声明与定义分离时的缺省参数只在声明中提供
void func3(int a, int b = 2, int c = 3); 

int main() 
{
    / 全缺省参数调用示例
    func1();           / 输出:a=1, b=2, c=3
    func1(10);         / 输出:a=10, b=2, c=3
    func1(10, 20);     / 输出:a=10, b=20, c=3
    func1(10, 20, 30); / 输出:a=10, b=20, c=30

    / 半缺省参数调用示例(必须从左向右传参)
    func2(100);          / 输出:a=100, b=2, c=3
    func2(100, 200);     / 输出:a=100, b=200, c=3
    func2(100, 200, 300); / 输出:a=100, b=200, c=300

    / 函数声明与定义分离的调用示例
    func3(1000);         / 输出:a=1000, b=2, c=3

    return 0;
}

/ 函数定义(不重复缺省值)
void func3(int a, int b, int c) 
{
    cout << "a=" << a << ",b=" << b << ",c=" << c << endl;
}

/ 错误示例1: 半缺省参数未从右向左连续缺省
/ void errorFunc1(int a = 1, int b, int c = 3) { } / 错误: 非连续缺省

/ 错误示例2: 函数声明和定义都提供缺省值
/ void func4(int a = 1); / 声明
/ void func4(int a = 1) { } / 错误: 定义时重复提供缺省值

​

​

六、函数重载

函数重载是面向对象编程中的一个重要概念, 它允许在同一作用域内定义多个同名但参数列表不同的函数,简单来说就是C++支持在同一作用域内出现同名函数,但要求参数列表必须不同(类型、数量或顺序
返回值类型不能单独作为重载依据,会产生歧义,编译器不知道调用哪个

#include <iostream>
using namespace std;

/1、参数类型不同
int add(int a, int b) 
{
    return a + b;
}

double add(double a, double b) 
{
    return a + b;
}

/2、参数个数不同
int add(int a, int b, int c) 
{
    return a + b + c;
}

/3、参数顺序不同
double add(int a, double b) 
{
    return a + b;
}

double add(double a, int b) 
{
    return a + b;
}
/ 返回值不同不能作为重载条件
/void func()
 /{}
 /
 /int func()
 /{ return 0;}
int main() 
{
    cout << add(1, 2) << endl;         
    cout << add(1.5, 2.5) << endl;    
    cout << add(1, 2, 3) << endl;  
    cout << add(1, 1.1) << endl;
    cout << add(2.2, 2) << endl;

    return 0;
}

七、引用

1.引用的概念和定义

  • 引用就是给已经存在的变量取别名,编译器不会为引⽤变量开辟内存空间, 它和它引用的变量共用同⼀块内存空间。引用和引用对象的关系就像自己的本名和小名,都指向自己
  • 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;

	/四个地址都相同
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;

	return 0;
}

2.引用的特性

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,再也不能引用其他实体
#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	/int& b;是不允许的,引用必须初始化
	int& b = a;

	int c = 20;
	/int& b = c;是不允许的,C++引用不能改变指向

	return 0;
}

3.引用的使用

  • 函数参数传递,通过引用传递参数可以避免对象复制,提高性能,尤其适用于大型对象,代码也很简单,使用起来很方便
/ 引用参数:直接修改原始对象
void swap(int& a, int& b) 
{
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 1, y = 2;
    swap(x, y);  / x 和 y 被直接修改
    cout << x << ", " << y;  / 输出: 2, 1
}
  • 函数返回值,返回引用可以避免返回对象时的复制开销,但需注意生命周期问题(避免返回局部变量的引用)。简单来说就是函数返回引用时可以直接修改返回值,但是得满足返回的是可修改的左值,以及引用对象在函数外部有效
int& getMax(int& a, int& b) 
{
    return (a > b) ? a : b;
}

int main() 
{
    int x = 5, y = 10;
    getMax(x, y) = 20;  / 修改最大值为 20
    cout << y;  / 输出: 20
}
  • 避免返回局部变量的引用:局部变量在函数结束后销毁,引用会变成悬空引用。
int& func() 
{
    int x = 10;
    return x;  / 错误:返回局部变量的引用
}

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

4. const引用

可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大

#include<iostream>
using namespace std;

int main()
{
	const int a = 10;
	/int& ra = a;是不允许的,const修饰a后,a为常量。
	/ 而这里的ra是可修改的变量,是对a的访问权限地放大
	const int& ra = a;
	/ra++;是不允许的,因为此时ra是常量

	int b = 20;
	const int& rb = b;
	/这里对rb进行const修饰是允许的
	/这里的引用是对b访问权限的缩小

	int c = 1;
	/int& rc = c * 3;是不允许的,c*3产生了一个临时的整数值
	/ C++中默认临时值为不可改变的常数,引用常数是对使用权限的放大
	/引用必须绑定到一个左值(可寻址的变量),而不能直接绑定到临时对象(右值)
	const int& rc = c * 3;

	double d = 1.1;
	/int& rd = d;是不允许的,double强制转换成int/得先额外拷贝整数部分,拷贝值为常量
	/引用常数是对使用权限的放大
	const int& rd = d;

	return 0;
}

5.指针和引用的关系

  • C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代
  • 语法概念上引用是一个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间
  • 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的
  • 引用在初始化时引用一个对象后,就不能再引用其他对象,而指针可以在不断地改变指向对象
  • 引用可以直接访问指向对象,指针需要解引用才是访问指向对象
  • sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
  • 指针很容易出现空指针和野指针的问题,引用很少出现,使用起来相对更安全⼀些

八、Inline

1.内联函数的使用

用inline修饰的函数叫做内联函数,在 C++ 中,inline 关键字用于向编译器建议将函数体直接替换函数调用,从而减少函数调用的开销

#include<iostream>
using namespace std;
/声明内联函数
inline int add(int a, int b)
{
	return a + b;
}
int main()
{
	int result = add(3, 4);
	/可能被编译器展开为:int result=3+4;
	return 0;
}
  • 使用inline可以避免压栈、跳转和返回操作,适用于简短函数。但inline对编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择不展开(如循环递归等复杂逻辑)
  • vs编译器debug版本下面默认是不展开inline的,这样方便调试
  • inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为编译器一旦将一个函数作为内联函数处理,就会在调用位置展开,即该函数是没有地址的,也不能在其他源文件中调用,故一般都是直接在源文件中定义内联函数的

2. 为什么需要内联函数

普通函数调用的开销太大

int square(int x) 
{ 
	return x * x; 
}
int a = square(5); 

/ 调用过程:
/ 1. 将参数压栈
/ 2. 跳转到函数地址
/ 3. 执行函数体
/ 4. 返回结果
/ 5. 恢复调用现场

​

内联展开的优化(效率得到了提高)

inline int square(int x) 
{ 
	return x * x; 
}
int a = square(5);  

/ 直接展开为:int a = 5 * 5;

3.宏函数&内联函数

宏函数通过#define实现,易出错,调试困难,且不进行类型检查

#define SQUARE(x) ((x) * (x))

内联函数

inline int square(int x) 
{ 
	return x * x; 
}

九、nullptr

在 C++ 中,nullptr 是一个关键字,用于表示空指针常量。它是 C++11 引入的特性,用于替代传统的 NULL 宏,解决了 NULL 在函数重载和模板推导中的二义性问题。

在C++中使用nullptr来完成C语言中null的工作

1.NULL的缺陷

在C++中,NULL通常被定义为整数0或(void*)0,这可能会导致函数重载歧义

void func(int x);       /版本1
void func(char* ptr);   /版本2

func(NULL);  
 /调用哪个版本?可能导致编译错误或意外调用
/NULL 被视为 int,可能错误调用 func(int)

2.nullptr的特性

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

/nullptr使用场景
int* ptr1 = nullptr;       /合法
char* ptr2 = nullptr;      /合法
int num = nullptr;         /非法:不能转换为整数


/解决函数重载歧义
void func(int x);      / 版本1
void func(char* ptr);  / 版本2

func(nullptr);   /明确调用 func(char* ptr)

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐