C++学习笔记:
C++ 进阶之路__Zwy@的博客-CSDN博客
各位于晏,亦菲们,请点赞关注!
我的个人主页:
_Zwy@-CSDN博客
目录
1、从C语言到C++的进化
1.1、历史渊源
1.2、语法层面的区别和联系
1.2.1、数据类型
1.2.2、函数定义与重载
1.2.3、变量声明与定义
1.2.4、面向对象特性
1.3、内存管理
1.4、编程范式
1.5、应用领域
2、C++的发展历史
2.1、前期探索与诞生(1979 年 - 1985 年)
2.2、标准化阶段(1991 年 - 1998 年)
2.3、现代 C++ 发展阶段(2003 年至今)
2.4、C++的参考文档
3、第一个C++程序
4、命名空间(重点)
4.1、命名空间的价值
4.2、namespace关键字
4.3、命名空间成员的访问
4.3.1、使用作用域解析运算符(::)
4.3.2、使用using声明
4.3.3、使用using namespace指令
4.4、嵌套命名空间
4.5、匿名命名空间
4.6、标准库命名空间std
5、C++输入输出(了解)
5.1、iostream 库概述
5.2、标准输出cout
5.3、标准出入cin
6、缺省参数(重点)
6.1、全缺省参数
6.2、半缺省参数
6.3、默认参数的顺序
6.4、声明和定义的一致性
7、函数重载(重点)
7.1、参数类型不同导致的重载
7.2、参数个数不同导致的重载
7.3、参数顺序不同导致的重载
7.4、函数重载的注意事项
7.4.1、返回值类型不能作为重载依据
7.4.2、重载函数的调用可能存在二义性
7.4.3、重载函数与默认参数的结合使用
8、引用(重点)
8.1、基本概念
8.2、引用的特性
8.2.1、必须初始化
8.2.2、一旦初始化就不能改变引用对象
8.2.3、⼀个变量可以有多个引用
8.3、引用的使用
8.3.1、引用作为函数参数
8.3.2、保证函数可以修改外部变量
8.3.3、引用作为函数返回值
8.4、const引用
8.4.1、定义
8.4.2、作为函数参数的const引用
8.4.3、const引用和临时对象
8.4.4、const引用作为函数返回值
8.5、引用和指针的关系
8.5.1、概念上的关联
8.5.2、初始化要求
8.5.3、操作符的使用
8.5.4、sizeof中含义不同
9、inline(了解)
9.1、inline函数的特点
9.1.1、代码膨胀与效率权衡
9.1.2、编译阶段的处理
9.1.3、作用域和链接特性
10、nullptr(重点)
10.1、NULL和nullptr
10.2、与NULL比较
10.3、语义清晰性
10.4、与0比较(指针上下文)
C++知识总结
tips:
本文目录中标记重点的知识,需要重点理解,尽量做到融会贯通,举一反三
标记了解的知识,现阶段只需要简单了解即可!
1、从C语言到C++的进化
1.1、历史渊源
C 语言诞生于 20 世纪 70 年代,是一种面向过程的编程语言。它以简洁高效的语法、对底层硬件的直接操控能力,迅速在系统编程、操作系统开发等领域占据了重要地位。C 语言的设计理念注重代码的执行效率和资源利用,其标准库提供了丰富的基础函数,能够完成诸如文件操作、内存管理等基本任务。
随着软件规模的不断扩大和编程需求的日益复杂,C++ 应运而生。C++ 在 C 语言的基础上进行了扩展,它保留了 C 语言的核心特性,同时引入了面向对象编程(OOP)的概念。20 世纪 80 年代初开始发展的 C++,逐渐融合了封装、继承和多态等面向对象特性,使得程序的设计更加模块化、可扩展和易于维护。这一发展使得 C++ 不仅适用于系统级编程,还广泛应用于游戏开发、图形图像处理、大型软件项目等众多领域。
1.2、语法层面的区别和联系
1.2.1、数据类型
在数据类型方面,C 语言和 C++ 有很多相似之处。它们都拥有基本数据类型,如整型(int)、浮点型(float、double)、字符型(char)等。然而,C++ 在 C 的基础上进行了一些扩展。例如,C++ 增加了布尔类型(bool),使得逻辑判断更加直观清晰。此外,C++ 还引入了引用类型(&),这是 C 语言所没有的。引用在函数参数传递和函数返回值等方面提供了一种更方便、更安全的方式,避免了指针操作可能带来的一些风险。
1.2.2、函数定义与重载
C 语言中的函数定义相对较为简单,函数名在一个作用域内必须唯一。而 C++ 支持函数重载,即允许在同一作用域内定义多个同名函数,只要它们的参数列表不同(参数个数、参数类型或参数顺序)。这一特性大大提高了代码的灵活性和可读性,使得程序员可以根据不同的参数需求定义同名函数来执行相似的操作。
1.2.3、变量声明与定义
C 语言中,变量的声明和定义通常是分开进行的,并且在代码块开头集中声明变量是一种常见的编程风格。而 C++ 则更加灵活,变量可以在需要使用的地方进行声明和定义,这使得代码的逻辑结构更加清晰,变量的作用域更加明确。
1.2.4、面向对象特性
这是 C++ 相对于 C 语言最为显著的扩展。C++ 的类(class)是面向对象编程的核心。通过类,可以将数据和操作数据的函数封装在一起,实现数据的隐藏和保护。
1.3、内存管理
在内存管理方面,C 语言和 C++ 都提供了直接操控内存的能力,如使用malloc和free函数在 C 语言中进行动态内存分配和释放,在 C++ 中则可以使用new和delete运算符。然而,C++ 在内存管理上引入了更多的安全性和便利性。例如,C++ 的构造函数和析构函数可以在对象创建和销毁时自动执行一些内存初始化和清理工作,减少了因手动管理内存而可能出现的错误。同时,C++ 11 引入的智能指针(如shared_ptr和unique_ptr)进一步增强了内存管理的安全性,能够自动处理对象的生命周期,避免了内存泄漏和悬空指针等问题,这是 C 语言所不具备的高级内存管理机制。
1.4、编程范式
C 语言主要遵循面向过程编程范式,程序的设计围绕着函数和数据结构展开,强调算法的实现和数据的处理流程。例如,一个简单的 C 语言程序可能是一系列函数的顺序调用,通过函数之间的数据传递来完成特定任务。
而 C++ 支持多种编程范式,除了面向对象编程外,还可以进行面向过程编程(兼容 C 语言风格)以及泛型编程。泛型编程通过模板(template)实现,允许编写与类型无关的代码,提高代码的复用性。例如,C++ 标准模板库(STL)中的容器(如vector、list等)和算法(如sort、find等)就是泛型编程的典型应用,它们可以处理不同类型的数据,大大提高了编程效率。
1.5、应用领域
由于其特性的不同,C 语言和 C++ 在应用领域上也有所侧重。C 语言凭借其高效性和对底层的操控能力,仍然广泛应用于操作系统开发(如 Linux 内核)、嵌入式系统开发(如单片机编程)、驱动程序开发等对性能和资源控制要求极高的领域。
C++ 则在游戏开发领域占据重要地位,许多大型游戏引擎(如 Unreal Engine)都是基于 C++ 开发的。同时,C++ 在图形图像处理(如 OpenCV 库)、数据库管理系统开发、金融领域的高性能计算等方面也有着广泛的应用,其面向对象特性和丰富的库支持使得开发大型复杂软件项目更加高效和便捷。
1.6、总结
比较项目 | C 语言 | C++ |
---|---|---|
编程范式 | 主要面向过程 | 支持面向过程、面向对象、泛型编程等多种范式 |
数据类型 | 基本数据类型(如 int、float、char 等),无布尔类型(常用宏定义模拟),无引用类型 | 基本数据类型基础上,增加布尔类型,有引用类型 |
函数定义与重载 | 函数名在同一作用域内必须唯一,无函数重载 | 支持函数重载,可根据参数列表不同定义同名函数 |
变量声明与定义 | 通常在代码块开头集中声明变量,声明和定义可分开 | 变量可在需要处声明和定义,更灵活 |
面向对象特性 | 无类、继承、多态等概念,可通过结构体和函数指针模拟简单面向对象特性,但不便捷 | 以类为核心实现封装、继承、多态等完整面向对象特性 |
内存管理 | 使用 malloc、free 函数进行动态内存分配与释放 | 除 new、delete 运算符外,C++11 引入智能指针(如 shared_ptr、unique_ptr)增强内存管理安全性 |
标准库 | 标准库提供基础函数,如文件操作、字符串处理等函数,但功能相对 C++ 标准库较基础 | 除了兼容 C 语言标准库部分,还有丰富的标准模板库(STL),如容器(vector、list 等)和算法(sort、find 等),提供大量通用数据结构和算法实现 |
应用领域 | 操作系统开发(如 Linux 内核)、嵌入式系统开发、驱动程序开发等对性能和资源控制要求极高的底层领域 | 游戏开发(如 Unreal Engine)、图形图像处理(如 OpenCV)、数据库管理系统开发、大型软件项目开发、金融领域高性能计算等 |
2、C++的发展历史
2.1、前期探索与诞生(1979 年 - 1985 年)
1979 年,Bjarne Stroustrup 在贝尔实验室开始基于 C 语言进行扩展,尝试开发一种新语言,当时他在分析 UNIX 内核时,面临着如何将内核模块化等问题,于是在 C 语言基础上增加了类似 Simula 的类机制,完成了一个可运行的预处理程序 Cpre,这便是 C++ 的前身 “C with Classes”
1983 年 8 月,第一个 C++ 实现投入使用,同年 12 月,Rick Mascitti 建议将其命名为 CPlusPlus,即 C++。
1985 年,Bjarne Stroustrup 正式发布了 C++ 语言的第一个版本,这个版本已经具备了类、继承、多态、虚函数等面向对象的特性,并且支持运算符重载、函数模板等高级特性。
2.2、标准化阶段(1991 年 - 1998 年)
1991 年,C++ 语言被 ANSI 和 ISO 标准化组织正式接受,并发布了 C++ 语言的第一个标准,该标准包含了类、继承、多态、虚函数、运算符重载、函数模板等面向对象特性,还涵盖了异常处理、命名空间、RTTI(运行时类型识别)等新特性。
1998 年,C++ 语言发布了第二个标准,其中包括了 STL(标准模板库)、智能指针等新特性,这使得 C++ 语言更加强大和灵活,进一步提升了其在软件开发中的应用价值。
2.3、现代 C++ 发展阶段(2003 年至今)
2003 年,C++ 语言发布了第三个标准,即现代 C++,此标准纳入了 TR1(技术报告 1)中的新特性,如正则表达式、智能指针、元编程等,让 C++ 在面对复杂的软件系统开发时更具优势。
2011 年,C++ 语言发布了第四个标准,引入了 lambda 表达式、右值引用等新特性,使 C++ 语言更加现代化和高效,增强了其在高性能计算和大型软件架构设计中的竞争力。
2014 年,C++ 语言发布了第五个标准,增加了多线程支持、类型推导等新特性,这使得 C++ 语言在并行计算和大数据处理领域能够更好地发挥其性能优势,满足了现代计算机系统对高效处理多任务和大量数据的需求 。
2020 年,C++20 标准发布,引入了模块 (modules)、协程 (coroutines)、范围 (ranges)、概念 (constraints) 等重大特性,还有对已有特性的更新,如 lambda 支持模板、范围 for 支持初始化等 。
2024 年 10 月,ISO 发布了最新的 C++23 标准,其包含了新的特性并且扩充了标准库,进一步推动了 C++ 语言的发展.
2.4、C++的参考文档
非官方文档:
只更新到C++11,内容以头文件形式展开,通俗易懂,对新手较为友好
cplusplus.com/reference/
C++官方中文文档:
包含C++目前的所有内容,但是不容易看懂,适合有一定基础的学者使用
C++ 参考手册 - cppreference.com
C++官方英文文档:
cppreference.com
3、第一个C++程序
C++兼容C语言
int main()
{
printf("Hello World!\n");
return 0;
}
C++兼容C语言绝大多数的语法,所以C语言实现的hello world依旧可以运行,C++中需要把定义文件代码后缀改为.cpp,VS编译器看到是.cpp就会调用C++编译器编译,Linux下要用g++编译,不再是gcc!
还是以我们熟知的"Hello world!"为例,来编写我们的第一个C++程序
#include<iostream>
int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}
其中std表示C++标准库的命名空间,cout代表输出流,endl表示换行,<iostream>是函数所需要的头文件, 看不懂没关系,后面我们会依次详细讲解!
程序输出结果:
4、命名空间(重点)
4.1、命名空间的价值
在 C++ 中,命名空间是一种用于组织代码的机制,它允许将全局作用域细分为不同的、有名字的作用域。其主要目的是防止命名冲突,尤其是在大型项目或使用多个库时使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,命名空间的出现就是针对这种问题的
例如:
#include<iostream>
int rand = 100;
int main()
{
std::cout << rand << std::endl;//编译出错 rand重定义,以前的定义是函数
}
在全局定义rand变量并赋值,在main函数中打印rand,这样写会导致编译出错,rand重定义,以前的定义是函数。
在 C++ 标准库中已经有一个名为rand的函数,它位于<cstdlib>头文件中(在 C++ 中,<cstdlib>里的函数等声明基本对应 C 语言标准库stdlib.h里的内容,rand函数用于生成伪随机数)。当你在代码中又定义了一个名为rand的全局变量时,就产生了命名冲突。编译器会分不清你这里的rand到底是要表示你自定义的那个整型变量,还是要调用标准库中的rand函数,所以会报错。
4.2、namespace关键字
在C++中,可以使用namespace关键字来定义命名空间
语法如下:namespace后面跟命名空间的名字,然后接⼀对{}即可,{}中即为命名空间的成员。命名空间中可以定义变量/函数/类型等。
namespace namespace_name
{
// 变量、函数、类等的定义
}
//例如
namespace _Zwy
{
int a = 10;
int b = 20;
int add(int x,int y)
{
return x + y;
}
}
4.3、命名空间成员的访问
4.3.1、使用作用域解析运算符(::)
这是最直接的访问方式。例如,要在main函数中调用_Zwy命名空间中的add函数,可以这样写:
#include<iostream>
int main()
{
int ret1 = _Zwy::add(10, 20);
int ret2 = _Zwy::add(100, 200);
std::cout << ret1 << std::endl;
std::cout << ret2 << std::endl;
return 0;
}
其中是std是C++标准库提供的命名空间,后面马上也会讲到
4.3.2、使用using声明
通过using声明,可以在特定的代码区域内直接使用命名空间中的成员,而不需要每次都使用作用域解析运算符。语法为using namespace_name::member_name;
例如:要在main函数中调用_Zwy命名空间中的add函数而不用每次都通过作用域解析符
#include<iostream>
using _Zwy::add;
using std::cout;
using std::endl;
int main()
{
int ret1 = add(10, 20);
int ret2 = add(100, 200);
cout << ret1 << endl;
cout << ret2 << endl;
return 0;
}
4.3.3、使用using namespace
指令
不推荐在头文件中使用,这种方式会将整个命名空间引入当前作用域,使得其中的所有成员都可以直接使用,无需作用域解析运算符。语法为using namespace namespace_name;
例如:通过using namespace 指令 标准库命名空间std和自定义命名空间_Zwy中的所有成员都可以直接使用,无需上面两种访问方式
using namespace _Zwy;
using namespace std;
int main()
{
cout << a << endl;
cout << b << endl;
cout << add(a, b) << endl;
return 0;
}
不过,这种方式可能会导致命名冲突。例如,如果在引入某个命名空间后,又有另一个命名空间也定义了同名变量,编译器就不知道该使用哪个值了。所以在头文件中一般不推荐使用using namespace指令。
4.4、嵌套命名空间
namespace outer
{
namespace inner
{
void nested_function()
{
std::cout << "This is a nested function." << std::endl;
}
}
}
要访问嵌套命名空间中的成员,可以使用多级作用域解析运算符outer::inner::nested_function();
int main()
{
outer::inner::nested_function();
return 0;
}
输出:
4.5、匿名命名空间
无名命名空间用于定义只在当前文件中可见的标识符,相当于创建了一个具有内部链接的命名空间。例如:
#include<iostream>
namespace
{
int local_variable = 10;
void local_function()
{
std::cout << "This is a local function." << std::endl;
}
}
int main()
{
local_function();
return 0;
}
输出:
无名命名空间中的成员不能在其他文件中访问,它们的作用类似于在文件作用域中使用static关键字(在 C++ 中,static关键字用于表示内部链接,但无名命名空间是更现代的实现方式)
4.6、标准库命名空间std
C++ 标准库中的所有标识符都定义在std命名空间中。例如,std::cout用于输出,std::vector是一个常用的容器类。
可以像访问其他命名空间成员一样访问标准库中的内容。也可以选择将std命名空间中的部分成员通过using声明引入,或者使用using namespace std;(虽然不推荐,因为可能导致命名冲突)来简化代码。例如:
使用作用域解析运算符
#include<iostream>
int mian()
{
std::cout << "作用域解析运算符" << std::endl;
return 0;
}
使用using声明
#include<iostream>
using std ::cout;
using std ::endl;
int main()
{
cout << "using 声明" << endl;
return 0;
}
使用using namespace std;(不推荐) 日常练习时使用即可
#include<iostream>
using namespace std;
int main()
{
cout << "using namespace std;" << endl;
return 0;
}
5、C++输入输出(了解)
5.1、iostream 库概述
在 C++ 中,输入输出操作主要依赖<iostream>库。<iostream> 是 Input Output Stream 的缩写这个库提供了用于处理标准输入(cin)、标准输出(cout)、标准错误输出(cerr)和无缓冲标准错误输出(clog)的对象和相关操作。
<iostream>库是基于流(stream)的概念进行设计的。流是字节序列的抽象,输入流用于从某个源读取字节序列,输出流用于将字节序列写入某个目标
5.2、标准输出cout
基本用法:
#include <iostream>
int main()
{
const char* message = "Hello World!";
int num = 100;
std::cout << message << num<< std::endl;
return 0;
}
输出:
在这里,<<是输出运算符,它将右侧的数据发送到cout流中。endl是一个操纵符,它的作用是输出一个换行符并刷新输出缓冲区
格式化输出:
使用setw操纵符设置输出宽度:setw来自<iomanip>头文件,用于设置下一个输出项的宽度。
例如:
#include <iostream>
#include <iomanip>
int main()
{
int num1 = 10;
int num2 = 100;
std::cout << std::setw(5) << num1 << std::endl;
std::cout << std::setw(5) << num2 << std::endl;
return 0;
}
输出:
setw(5)会使得输出的整数占 5 个字符的宽度,如果整数本身宽度小于 5,则在前面填充空格。
控制浮点数的精度:
可以使用fixed和setprecision操纵符来控制浮点数的输出精度。fixed来自<iostream>,setprecision来自<iomanip>。例如:
#include <iostream>
#include <iomanip>
int main()
{
double d = 3.1415926;
std::cout << std::fixed << std::setprecision(2);
std::cout << d << std::endl;
return 0;
}
输出:
这里fixed表示以固定小数点数形式输出,setprecision(2)表示小数部分保留两位精度,所以输出结果为3.14
5.3、标准出入cin
可以使用cin
来读取用户输入的数据。例如,读取一个整数和一个字符串:
#include <iostream>
int main()
{
int age;
std::string name;
std::cout << "Please enter your name: ";
//输入姓名
std::cin >> name;
std::cout << "Please enter your age: ";
//输入年龄
std::cin >> age;
std::cout << "Your name is " << name << " and your age is " << age << std::endl;
return 0;
}
输出:
在这里,>>是输入运算符,它从cin流中读取数据并存储到相应的变量中。
输入中的问题及处理:
缓冲区问题:
cin在读取数据时是基于缓冲区的。当用户输入的数据多于程序期望读取的数据时,多余的数据会留在缓冲区中。例如,在读取一个整数后接着读取一个字符串,如果用户在输入整数后输入了一个空格和其他字符,那么在读取字符串时,可能会读取到意外的数据。解决这个问题的一种方法是使用cin.ignore()函数来清除缓冲区中的多余字符。例如:
#include<string>
#include <iostream>
int main()
{
int num;
std::string text;
std::cin >> num;
std::cin.ignore(); // 忽略缓冲区中的一个字符(通常是换行符)
std::cout << "Please enter a string: ";
std::getline(std::cin, text);
std::cout << "The number is " << num << " and the string is " << text << std::endl;
return 0;
}
类型不匹配问题:
如果用户输入的数据类型与程序期望读取的数据类型不匹配,cin会进入错误状态。例如,程序期望读取一个整数,但用户输入了一个字母。可以通过检查cin的状态来处理这种情况。例如:
#include <iostream>
int main()
{
int number;
std::cout << "Please enter an integer: ";
std::cin >> number;
if (std::cin.fail())
{
std::cout << "Invalid input. Please enter a valid integer." << std::endl;
std::cin.clear(); // 清除错误状态
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 忽略缓冲区中的所有字符直到换行符
}
else
{
std::cout << "You entered " << number << std::endl;
}
return 0;
}
以上关于cin和cout的讲解,大家可能都不太懂,现阶段只需要掌握如何使用cin和cout进行输入输出即可,后面我们会有专门的章节讲解IO流库,会对C++的IO流做更深入的讲解
6、缺省参数(重点)
缺省参数(也称为默认参数)是 C++ 中的一个特性,它允许在函数声明或定义时为参数指定默认值。这样,在调用函数时,如果没有为这些参数提供实际的值,编译器就会自动使用默认值。缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
6.1、全缺省参数
全缺省就是全部形参给缺省值
#include <iostream>
void Test(std::string s = "helloworld!", int num = 3)
{
for (size_t i = 0; i < num; ++i)
{
std::cout << s <<std:: endl;
}
}
int main()
{
Test();
return 0;
}
输出:
也可以调用函数时传递参数,覆盖其中一个缺省参数
#include <iostream>
void Test(std::string s = "helloworld!", int num = 3)
{
for (size_t i = 0; i < num; ++i)
{
std::cout << s <<std:: endl;
}
}
int main()
{
Test("hello_Zwy!");
return 0;
}
输出:
同样可以将两个缺省值全覆盖
#include <iostream>
void Test(std::string s = "helloworld!", int num = 3)
{
for (size_t i = 0; i < num; ++i)
{
std::cout << s <<std:: endl;
}
}
int main()
{
Test("hello_Zwy!",5);
return 0;
}
输出:
6.2、半缺省参数
#include <iostream>
void Test(std::string s , int num = 3, char ch='a')
{
for (size_t i = 0; i < num; ++i)
{
std::cout << s <<ch<<std:: endl;
}
}
int main()
{
Test("helloworld!");
return 0;
}
输出:
同样缺省值也可以进行覆盖,使用传参时实际的参数
#include <iostream>
void Test(std::string s , int num = 3, char ch='a')
{
for (size_t i = 0; i < num; ++i)
{
std::cout << s <<ch<<std:: endl;
}
}
int main()
{
Test("helloC++!!!",2,'C');
return 0;
}
输出:
6.3、默认参数的顺序
C++规定,当函数有多个参数时,缺省参数必须从右往左依次定义。
例如,下面的函数声明是正确的
void Test(int a, int b = 2, int c = 3);
但是,下面的声明是错误的
void Test(int a = 1, int b, int c = 3);
因为我们说,缺省参数必须从右往左依次定义,不能跳跃!
6.4、声明和定义的一致性
如果函数的声明和定义分开,那么缺省参数只能在函数声明中指定一次。通常是在头文件中的函数声明处指定缺省参数
头文件(Test.h)
#ifndef TEST_H
#define TEST_H
void myTest(int a, int b = 2);
#endif
源文件(Test.cpp)
#include "Test.h"
void myTest(int a, int b)
{
// 函数体
}
如果在源文件中的函数定义处也指定了不同的默认参数,编译器可能会发出警告或者错误,因为这会导致不一致性
7、函数重载(重点)
函数重载是 C++ 中的一个特性,它允许在同一作用域内定义多个同名函数,只要这些函数的参数列表(参数个数、参数类型或参数顺序)不同即可。编译器会根据函数调用时提供的实际参数来确定调用哪个具体的重载函数。C语言是不支持同⼀作用域中出现同名函数的。
例如,定义两add函数来完成不同类型的加法操作
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}
7.1、参数类型不同导致的重载
这是最常见的函数重载情况。如上述add函数的例子,一个用于整数相加,一个用于双精度浮点数相加。当在程序中调用add函数时,编译器会根据传入的参数类型来决定调用哪个版本。
例如:
int main()
{
int ret1 = add(3, 5);
double ret2 = add(3.14, 2.71);
return 0;
}
在add(3, 5)调用中,编译器看到参数是两个整数,所以会调用int add(int a, int b)这个版本;而在add(3.14, 2.71)调用中,由于参数是两个双精度浮点数,编译器会调用double add(double a, double b)版本
7.2、参数个数不同导致的重载
函数也可以根据参数个数的不同进行重载。例如,定义一个函数用于打印一个整数,另一个函数用于打印两个整数:
void print(int num)
{
std::cout << "The number is: " << num << std::endl;
}
void print(int num1, int num2)
{
std::cout << "The two numbers are: " << num1 << " and " << num2 << std::endl;
}
int main()
{
print(5);
print(3, 7);
return 0;
}
编译器会根据传递的参数个数来选择合适的print函数。print(5)会调用void print(int num),而print(3, 7)会调用void print(int num1, int num2)
7.3、参数顺序不同导致的重载
即使参数个数和类型的组合总数相同,但顺序不同也可以构成函数重载。
例如:
void Test(int a, double b)
{
std::cout << "int and double: " << a << ", " << b << std::endl;
}
void Test(double a, int b) {
std::cout << "double and int: " << a << ", " << b << std::endl;
}
int main()
{
Test(3, 4.5);
Test(3.14, 7);
return 0;
}
对于Test(3, 4.5),编译器会调用void Test(int a, double b),因为参数顺序是先整数后双精度浮点数;而Test(3.14, 7)会调用voidTest(double a, int b),因为参数顺序是先双精度浮点数后整数
7.4、函数重载的注意事项
7.4.1、返回值类型不能作为重载依据
返回值类型不能作为重载依据在 C++ 中,仅返回值类型不同的函数不能构成重载
例如:下面的代码是错误的,编译不能依靠返回值类型来判断为函数重载
int add(int a, int b)
{
return a + b;
}
double add(int a, int b)
{
return (double)(a + b);
}
7.4.2、重载函数的调用可能存在二义性
如果存在多个重载函数,其参数匹配情况不明确时,编译器会报二义性错误。例如
void Test(int a, double b)
{
//...
}
void Test(double a, int b)
{
//...
}
int main() {
Test(3.0, 4.0);//编译出错,函数调用有二义性
return 0;
}
在func(3.0, 4.0)这个调用中,两个参数都是双精度浮点数,编译器无法确定是将第一个参数当作整数、第二个参数当作双精度浮点数来匹配void func(int a, double b),还是将第一个参数当作双精度浮点数,第二个参数当作整数来匹配void func(double a, int b),所以会产生二义性错误
7.4.3、重载函数与默认参数的结合使用
当函数有默认参数时,可能会影响函数重载的调用。例如:
void print(int a, int b = 0)
{
std::cout << "Function with default parameter: " << a << ", " << b << std::endl;
}
void print(int a)
{
std::cout << "Function without default parameter: " << a << std::endl;
}
int main()
{
print(3);
return 0;
}
这段代码会导致编译错误,因为在调用print(3)时,编译器不知道匹配哪个重载函数,因为第一个函数有缺省值,可以匹配,第二个函数也同样符合传参规则可以匹配! 编译器不知道该调用哪个函数
8、引用(重点)
8.1、基本概念
在 C++ 中,引用(Reference)是一种给变量起别名的机制。引用不是新定义⼀个变量,而是给已存在变量取了⼀个别名,引用就像是一个变量的另一个名字,它和被引用的变量共享同一块内存空间。比如水浒传中的林冲,外号豹子头,实际上林冲和豹子头都是同一个人
基本语法:
类型& 引用别名=引用对象;
int main()
{
int num = 10;
int& ref = num;
return 0;
}
这里ref是num的引用,对ref的任何操作,实际上就是对num的操作。比如,ref = 20;会将num的值修改为20。
8.2、引用的特性
8.2.1、必须初始化
int main()
{
int& uninitialized_ref;//编译错误
return 0;
}
这样写是错误的,引用在定义时必须指明引用对象,初始化
8.2.2、一旦初始化就不能改变引用对象
int main()
{
int num1 = 10;
int num2 = 20;
int& ref = num1;
ref = num2; // 这里不是让ref引用num2,而是将num2的值赋给ref所引用的num1
return 0;
}
ref在初始化时已经引用了num1,就不能在引用其他对象,这里是把num2的值赋给ref,这会导致ref引用的num1的值也跟着变化
8.2.3、⼀个变量可以有多个引用
int main()
{
int num = 10;
int& ref1 = num;
int& ref2 = num;
int& ref3 = num;
return 0;
}
实际上num和ref1,ref2,ref3,指向同一块内存空间,其中一个变量的值被改变,其他变量的值也会随之改变
8.3、引用的使用
8.3.1、引用作为函数参数
传递引用可以避免复制开销,当函数的参数是比较大的对象(如结构体、类对象,后面我们会讲到C++的类和对象)时,使用引用传递可以避免对象的复制,提高函数的执行效率。例如,有一个结构体Person
struct Person
{
std::string name;//姓名
std::string gender;//性别
int age;//年龄
std::string id;//身份证号码
};
如果在函数传参时传值传参,会产生拷贝,降低程序的执行效率,比如:
void modifyAgeValue(Person p, int new_age)
{
p.age = new_age;
}
当调用这个函数时,Person对象会被复制一份传入函数,函数内对p的修改不会影响到原来的对象
如果使用引用传参,则可以减少拷贝,也可以直接对原对象做修改
void modifyAgeReference(Person& p, int new_age)
{
p.age = new_age;
}
这里p是Person对象的引用,调用这个函数时,不会复制Person对象,函数内对p的修改会直接影响到原来的Person对象。
8.3.2、保证函数可以修改外部变量
引用可以让函数修改传入的变量。例如
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
实现swap函数交换两个数的值,之前我们的解决方法是传递指针,对指针解引用,这样才能修改形参从而更改实参,现在我们可以直接传引用直接对原对象修改
8.3.3、引用作为函数返回值
返回引用可以用于左值表达式当函数返回引用时,返回的结果可以像左值(可以出现在赋值语句左边的表达式)一样使用。例如:
int& getValue(int* arr, int index)
{
return arr[index];
}
假设我们有一个整数数组int arr[] = {1, 2, 3};,可以这样使用这个函数:getValue(arr, 1) = 4;,这会将数组arr中的第二个元素修改为4,并返回。
注意事项:
返回引用时要确保引用的对象在函数返回后仍然存在。例如,返回一个局部变量的引用是错误的,因为局部变量在函数结束后就销毁了,例如:
int& Test()
{
int local_num = 10;
return local_num; // 错误,返回了局部变量的引用
}
8.4、const引用
8.4.1、定义
const引用是指对常量的引用。在 C++ 中,使用const关键字来修饰引用,它可以引用一个常量对象或者一个不能被修改的对象
const int num = 10;
const int& ref = num;
主要用于函数参数传递和保护数据不被意外修改。当我们不希望在函数内部修改引用所指向的对象时,可以使用const引用。这样可以提高程序的安全性和可读性。
8.4.2、作为函数参数的const
引用
当函数的参数是一个大型对象(如自定义的类或结构体),并且函数不需要修改这个对象时,使用const引用作为参数可以避免不必要的对象复制,同时保证对象不被修改。例如,假设有一个Person结构体:
struct Person
{
std::string name;//姓名
std::string gender;//性别
int age;//年龄
std::string id;//身份证号码
};
定义一个函数来打印Person对象的信息,我们不希望函数在打印信息的过程中对Person对象做任何修改,这时我们可以把参数设置为const引用
void printPerson(const Person& p)
{
std::cout << "Name: " << p.name <<" "<<"gender :"<<p.gender<<" " << " Age : " << p.age <<
" "<<"id:"<<p.id<< std::endl;
}
int main()
{
struct Person p = { "_Zwy","男",18,"11223344" };
printPerson(p);
return 0;
}
这里printPerson函数接收一个const引用参数p,在函数内部不能修改p所引用的Person对象。
如果函数参数是普通(非const)引用,函数内部可以修改引用所指向的对象。而const引用则限制了这种修改,保护了对象,提高程序的安全性
8.4.3、const引用和临时对象
临时对象的产生
在 C++ 中,当使用一个临时对象(例如一个临时的表达式结果)初始化一个引用时,通常需要使用const引用。例如:
int add(int a, int b)
{
return a + b;
}
int main()
{
const int& result_ref = add(3, 5);
return 0;
}
这里add(3, 5)返回一个临时对象,当我们想要用引用接收这个临时对象时,必须使用const引用。因为临时对象是一个右值(不能出现在赋值语句左边的表达式),普通引用不能绑定到右值,而const引用可以。(关于右值和左值在C++11中我们有详细讲解)
延长临时对象的生命周期
const引用的一个重要特性是它可以延长临时对象的生命周期。通常,一个临时对象在包含它的完整表达式结束后就会被销毁。但是,当这个临时对象被绑定到一个const引用时,它的生命周期会延长到与这个const引用的生命周期相同。例如:
const std::string& getTempString()
{
std::string str = "HelloWorld!";
return str;
}
int main()
{
const std::string& str_ref = getTempString();
std::cout << str_ref << std::endl;
return 0;
}
在这个例子中,getTempString函数返回一个局部的string对象str的const引用。正常情况下,str在getTempString函数结束后就应该被销毁。但是因为它被绑定到了main函数中的const引用str_ref,所以它的生命周期延长到了str_ref的生命周期结束,这样在main函数中就可以正确地使用str_ref来访问这个字符串。
8.4.4、const引用作为函数返回值
当函数返回一个对象,并且不希望调用者通过返回值来修改这个对象时,可以返回一个const引用。
class Matrix
{
public:
const int& getElement(int row, int col) const
{
// 返回矩阵中指定行和列的元素
return elements[row * cols + col];
}
private:
int* elements;
int rows;
int cols;
};
这里getElement函数返回一个const引用,这样调不能通过返回值来修改矩阵的元素
与返回普通引用的对比:
如果返回普通引用,就可以修改返回值所引用的对象。例如,如果getElement函数返回普通引用,像matrix.getElement(0, 0) = 10;这样的操作就可以修改矩阵元素,而返回const引用则禁止了这种修改。
8.5、引用和指针的关系
8.5.1、概念上的关联
指针:指针是一个变量,其值为另一个变量的地址。指针定义了一个可以存储整数变量地址的指针变量。通过&运算符可以获取变量的地址。
引用:引用是一个变量的别名,它不是一个新的变量,而是和被引用的变量共享同一块内存空间。
8.5.2、初始化要求
指针:指针可以先定义后初始化,也可以在定义时初始化,指针还可以初始化为nullptr(在 C++ 11 及以后),表示不指向任何有效地址
引用:引用在定义时必须初始化,而且一旦初始化就不能再引用其他变量。
8.5.3、操作符的使用
指针:使用*运算符来访问指针所指向的变量的值,这个操作称为解引用。 可以通过指针来修改它所指向的变量的值。
引用:引用的使用和普通变量几乎一样,因为它就是变量的别名。
8.5.4、sizeof中含义不同
引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)
比如:
int main()
{
int a = 100; double b= 3.14;
int* p1 = &a; double* p2 = &b;
int& ref1 = a; double& ref2 = b;
//int类型
cout <<"int引用大小:"<< sizeof(ref1) << endl;
cout <<"指针大小"<< sizeof(p1) << endl;
//double类型
cout << "double引用大小"<<sizeof(ref2) << endl;
cout << "指针大小:"<<sizeof(p2) << endl;
return 0;
}
输出:
9、inline(了解)
在 C++ 中,inline是一个关键字,用于建议编译器将函数体插入到函数调用处,而不是像普通函数调用那样进行跳转执行。这样做的主要目的是减少函数调用的开销,特别是对于一些短小的函数,可能会提高程序的执行效率。
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要不建里栈帧了,就可以提高效率。
inline int add(int a, int b)
{
return a + b;
}
当编译器处理add函数的调用时,如int result = add(3, 5);,它可能会将add函数的代码直接替换到调用处,就好像写成了int result = 3 + 5;一样,而不是产生一个函数调用的机器指令。
9.1、inline函数的特点
9.1.1、代码膨胀与效率权衡
当函数被声明为inline时,在多个地方调用这个函数会导致代码膨胀。因为编译器会在每个调用点插入函数体的代码。例如,如果add函数在程序中被调用了 100 次,那么编译器可能会在 100 个不同的地方插入return a + b;这行代码。
不过,对于短小的函数,这种代码膨胀可能不会造成太大的问题,而且由于避免了函数调用的开销(如保存和恢复寄存器、栈帧的建立和销毁等),可能会提高程序的运行速度。但对于复杂的、代码量较大的函数,过度使用inline可能会导致程序体积过大,甚至可能因为指令缓存命中率降低等原因而降低性能。
9.1.2、编译阶段的处理
inline函数的处理主要在编译阶段。编译器有权决定是否真正将函数内联,它会根据函数的复杂程度、调用频率、优化级别等因素综合考虑。即使函数被声明为inline,编译器也可能不进行内联处理。例如,对于包含复杂的循环、递归或者大量控制流语句的函数,编译器可能会认为内联并不合适。
9.1.3、作用域和链接特性
inline函数具有内部链接(在未使用extern关键字的情况下)。这意味着如果在一个源文件中定义了一个inline函数,它在其他源文件中是不可见的,除非通过extern关键字将其声明为外部链接。这种特性与普通函数不同,普通函数默认是外部链接,可以在多个源文件之间共享。
10、nullptr(重点)
10.1、NULL和nullptr
#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); 调用会报错。
nullptr是 C++ 11 引入的一个关键字,用于表示空指针。它是一个特殊的字面值,专门用于初始化指针,表示指针不指向任何有效的对象或函数
它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型
10.2、与NULL比较
void Test(int* p)
{
cout << "void func(int* p) " << endl;
}
void Test(int i)
{
cout << "void func(int i) " << endl;
}
int main()
{
Test(NULL); // 可能会调用func(int i),这可能不是期望的结果
Test(nullptr); // 一定会调用func(int* p),符合预期
return 0;
}
输出结果:
10.3、语义清晰性
nullptr更明确地表示一个指针值,它的语义就是 “空指针”。而NULL的定义可能因编译器和环境的不同而有所差异(虽然通常定义为((void*)0),但不是所有情况都如此),这可能会导致代码的可读性和可维护性降低。
10.4、与0比较(指针上下文)
在 C++ 中,整数0可以隐式转换为指针类型的空值,但这也可能会引起混淆。例如,当阅读代码int* p = 0;时,可能不太直观地理解这是在将指针初始化为空。而nullptr则更加明确地表示这是一个空指针的初始化操作
void Test(int* p)
{
cout << "void func(int* p) " << endl;
}
void Test(int i)
{
cout << "void func(int i) " << endl;
}
int main()
{
int* p = 0;
Test(p); // 0可以转换成空指针
Test(nullptr); // 一定会调用func(int* p),符合预期
return 0;
}
输出:
C++知识总结
本文我们从C语言和C++的历史渊源讲起,介绍了其从 C 语言进化而来的相关内容,涵盖发展历史、第一个C++小程序,重点讲解命名空间、输入输出、缺省参数、函数重载、引用、inline 函数、nullptr 等多方面特性,基本构建起 C++ 的基础知识体系,为之后的C++探索之旅打下坚实基础!
如上的讲解只是我的一些拙见,如有不足之处,还望各位大佬不吝在评论区予以斧正,感激不尽!创作不易,还请多多互三支持!你们的支持是我最大的动力!