C++基础入门(一)(基础无废话)
C和C++基础入门(一)(基础无废话)
目录
6.extern关键字实现C++程序同时兼容C环境和C++环境
前言
本文提供简明实用的C++入门教程,重点讲解核心概念并辅以实际代码示例。适合具备C语言等编程基础的读者快速掌握C++基础语法。作为系列教程的第一篇,本文主要介绍C++面向过程编程,后续将陆续推出:
- (二)面向对象编程
- (三)泛型编程
- (四)C++11及以上版本的新特性
通过循序渐进的学习路线,从零基础逐步进阶C++编程并掌握C++11及以上版本常用的新特性,快速全面掌握C++语言。
C++是一种通用的、编译型、静态类型的编程语言。它支持面向对象编程、泛型编程和过程式编程,提供了对底层内存的直接操作能力。C++具有高效运行性能,广泛用于系统软件、游戏开发、嵌入式系统等领域。其标准库提供了丰富的容器、算法和输入输出功能,并通过模板机制实现了高度抽象的泛型编程范式。
1.第一个简单的C++语言示例程序
以下为第一个C++语言编写的简单的示例程序:
#include<iostream> // 头文件,类似于C语言中的头文件或其他语言中的包等概念
// main函数,C++程序都是从这里开始执行的
int main()
{
// 输出语句,将"hello world"输出到控制台并换行
std::cout << "hello world" << std::endl;
// main函数运行结束
return 0;
}
这是一个比较简单的C++程序,但复杂的C++程序都是一样从main函数开始运行,在这种简单程序扩展出来的。
2.C或C++程序从源代码到二进制可执行文件的编译过程
上述简单的C++程序并不能被计算机直接识别,C++语言是一门高级语言,便于用户编写但计算机无法识别,计算机的CPU只能看得懂二进制指令,因此编写的C++源代码还要经过一系列编译过程生成二进制的可执行文件,才能被计算机执行。包括以下几个编译步骤:
预处理(.i后缀展开后的源代码文件)->编译(.s后缀的汇编语言文件)->汇编(.o后缀的二进制目标文件)->链接(.exe后缀的可执行文件),对于多个源文件进行编译时,通常会将它们分别编译成各自的目标文件,然后通过链接器将这些目标文件和所需的库文件链接在一起,生成最终的可执行文件。
- 预处理:编译器会首先处理源文件(.cpp 文件)。对于源文件中的每个 #include 指令,编译器都会查找并包含相应的头文件(.h 或 .hpp 文件)直接进行展开,执行完所有#预处理指令进行处理,此时会生成经过预处理后的.i后缀的源代码。
- 编译:将经过预处理后的.i后缀的文件进行编译成汇编语言,最后会得到一个.s后缀的汇编语言文件。
- 汇编:将经过汇编后的汇编语言文件进行汇编得到一个.o后缀的二进制目标文件,此时计算机还是无法直接执行该文件。
- 链接:符号链接所有源文件各自生成的目标文件,并链接所有目标文件中使用的C/C++的标准库文件,包括静态库和动态库,一般是动态库文件,如<iostream>中std::cout、<stdio.h>中printf函数的库文件,最后生成一个.out后缀的二进制可执行文件。
预处理过程中,编译器会执行以#开头的预处理指令,常见的预处理指令如下:
#define:定义一个宏,当宏有替换值时,在预处理阶段进行替换,否则默认删除。还可以定义一个带()的复杂表达式计算方式的宏,使用时直接类似于函数调用方式在()内传入值或变量,甚至还可以定义可变参数宏,用于可变参数的函数调用。示例代码如下:
// 定义一个宏,并且有替换值
#define A 100 // 定义一个宏A,在预处理阶段将展开源文件中的宏A替换为100
// 定义一个宏,无替换值,一般在程序中用于进行说明
#define _int_ // 定义一个宏_int_,在预处理阶段将宏_int_直接删除
int _int_ a = A;
// 定义一个复杂表达式宏,预处理阶段会将其进行替换,使用时类似于函数调用方式在括号内传入值或变量
#define RELWASE(x) delete x; x = nullptr; // RELEASE(a)
#define SUM(X,Y) (X+Y) // SUM(100,200)
#define BIGGER(X,Y) (X>Y?X:Y) // BIGGER(x,y)
// 定义一个可变参数表达式宏,允许通过不定参数方式使用预定义的可变参数函数
// 宏中使用...表示不定参数,可变参数函数使用__VA__ARGS__接收
#define TRACE(a, b,...) trace(a, b, __VA_ARGS__)
#undef:取消一个#define已经定义的宏
// 取消一个已定义的宏
#undef A
-
#ifdef 宏 ... #else ... #endif:类似于if-else语句,在检查宏存在或不存在的情况下执行内部代码 -
#ifndef 宏 ... #else ... #endnif:类似于if-else语句,在检查宏不存在或存在的情况下执行内部代码 -
#if 条件判断 ... #else ... #endif:类似于if-else语句,在条件判断中添加if-else相同写法的条件判断语句 -
#if 条件判断 ... #elif 条件判断... #else ... #endif:类似于if-else if-else语句,elif条件判断可任意个
示例代码如下:
// 常见的某个头文件仅包含一次的定义方式
#ifndef _HEAD_
#define _HEAD_
#include "head.h"
#endif
// 常见的定义在.c后缀的C函数的头文件兼容写法
#ifdef _cplusplus
extern "C" {
#endif
#include "c.h"
int ave(int, int); // C函数声明
#ifdef _cplusplus
}
#endif
- 编译器遵循C++标准添加定义的宏,不同编译器实现不一样,定义的宏可能不一样


3.C或C++程序运行时内存存储区域
在C++中,上述编写的C++程序经过编译后生成的二进制文件会首先存放在磁盘中,在运行时才会加载到内存中,在内存存储通常可以大致分为几个区域,这些区域根据存储的数据类型、生命周期和作用域来划分。这些区域主要包括:
1.代码区(Code Segment/Text Segment):
存储程序执行代码(即机器指令)的内存区域。这部分内存是共享的,只读的,且在程序执行期间不会改变。举例说明:当你编译一个C++程序时,所有的函数定义、控制结构等都会被转换成机器指令,并存储在代码区。
2.全局/静态存储区(Global/Static Storage Area):
存储全局变量和静态变量的内存区域。这些变量在程序的整个运行期间都存在,但它们的可见性和生命周期取决于声明它们的作用域。
①全局变量的可见性是整个程序,生命周期是整个程序运行期间。
②静态全局变量的可见性是声明它们的文件,生命周期是整个程序运行期间。
③静态局部变量的可见性是它们被声明的区域内部,生命周期是整个程序运行期间。
举例说明:全局变量(在函数外部声明的变量)和静态变量(static关键字声明的变量,无论是在函数内部还是外部)都会存储在这个区域。
3.栈区(Stack Segment):
存储局部变量、函数参数、返回地址等的内存区域。栈是一种后进先出(LIFO)的数据结构,用于自动释放存储函数调用和自动存储和释放局部变量。直接定义的普通变量就属于局部变量,存储在栈区,可以实现自动存储和释放,当函数或代码块被调用时,也会自动存储和运行结束后自动释放。
举例说明:在函数内部声明的变量(不包括静态变量)通常存储在栈上。当函数被调用时,其参数和局部变量会被推入栈中;当函数返回时,这些变量会从栈中弹出,其占用的内存也随之释放。
4.堆区(Heap Segment):
由程序员通过动态内存分配函数(如new和malloc)分配的内存区域,系统不会管理。堆区的内存分配和释放是使用(delete和free)手动的,因此程序员需要负责管理内存,以避免内存泄漏或野指针等问题,或者考虑使用智能指针只需要创建,会自动释放。
举例说明:当你使用new操作符在C++中动态分配一个对象或数组时,分配的内存就来自堆区。使用delete操作符可以释放堆区中的内存。
5.常量区(Constant Area):
存储常量(如字符串常量、const修饰的全局变量等)的内存区域。这部分内存也是只读的,且通常在程序执行期间不会改变。
举例说明:在C++中,使用双引号括起来的字符串字面量通常存储在常量区。此外,使用const关键字声明的全局变量,如果其值在编译时就已确定,也可能存储在常量区。
示例代码:
#include<iostream>
int globel = 10; // 全局变量
int main()
{
static int static1 = 20; // 静态变量
const int const1 = 30; // 常量
int local = 40; // 栈区局部变量
int* pint = new int(50); // new开辟在堆区变量
delete pint; // 堆区变量使用完后必须显式释放,避免内存泄漏
std::cout << "hello world" << std::endl; // "hello world"字面量常量
return 0;
}
4.C++源代码结构介绍
在C++中复杂的程序通常会分为头文件和源文件,头文件(.h文件/.hpp文件)和源文件(.c文件/.cpp文件)是组织代码的重要部分,它们共同工作以构建程序。这种分离有助于模块化和代码重用,同时也使得编译过程更加高效。
- 头文件(.h 或 .hpp),头文件主要用于声明(declarations),包括:
- 类(class)的声明
- 函数(functions)的原型(prototypes)
- 模板(templates)的声明
- 宏定义(#define)
- 外部变量(extern variables)的声明:extern 变量类型 变量名;
- 内联函数(inline functions)
- 源文件(.c/.cpp)包含声明在头文件的具体实际的代码实现,即函数体、类的成员函数的实现等。源文件通常包括必要的头文件,以便编译器知道它们正在使用的函数、类等是如何声明的。
头文件global.h示例代码:
#pragma once
extern int global_variable; // 声明一个全局变量
void print_global_variable(); // 声明一个函数,用于打印全局变量
源文件global.cpp示例代码:需要显式包含头文件global.h,并最好和头文件global同名
#include "global.h"
#include <iostream>
// 对头文件中声明的全局变量global_variable进行定义
int global_variable = 20;
// 对头文件中声明的函数print_global_variable进行定义
void print_global_variable() {
// 打印全局变量的值
std::cout << "Global Variable: " << global_variable << std::endl;
}
注意在头文件进行变量的定义时,头文件在被不同源文件包含而导致变量在不同源文件重复定义,编译的链接过程就会出现同变量重定义问题,因此在头文件只能进行外部变量的声明而不是定义,采用关键字extern声明为外部变量不在此处头文件定义,并且不进行初始化定义,而在源文件中进行变量的定义初始化,同理函数、类、模板等在头文件只进行声明但可以不使用关键字extern,在源文件进行实现。
头文件通常包含预处理指令宏定义#开头,如#ifndef、#define、#endif和#pragma once,这些指令用于防止头文件被重复包含(也称为“头文件保护”或“包含卫士”)而出现的重定义问题,两种方式选择一种即可,最常见的是使用#pragma once。
# 第一种防止头文件重复包含方式
#pragma once //声明编译器检查只包含一次该头文件
# 第二种防止头文件重复包含方式
#ifndef GLOBAL //如果没有定义头文件宏GLOBAL
#define GLOBAL //定义头文件宏GLOBAL
#include "global.h"
#endif //预处理指令宏定义#ifndef结束
5.C++变量作用域
在C++中,变量作用域(Scope)指的是程序中变量可以被访问的代码区域。作用域决定了变量的生命周期和可见性,对于栈区变量离开作用域不可见就会自动释放或析构,堆区变量需要在离开作用域前需要手动释放或析构。几种常见的变量作用域类型:
- 全局作用域:在函数外部声明的变量具有全局作用域。它们可以在程序的任何地方被访问,但通常建议在需要时才使用全局变量,因为它可能导致代码难以理解和维护,多线程时共享使用属于临界资源需要互斥使用。
- 局部作用域:在函数内部、代码块(如if语句、for循环等)内部声明的变量具有局部作用域。它们只能在声明它们的代码块内被访问。一旦离开该代码块,这些内部声明的变量就不再可见并且释放或析构。
- 命名空间作用域:在命名空间中声明的变量(实际上是实体,如变量、函数等)具有命名空间作用域。它们只能在相应的命名空间内被直接访问,但可以通过使用命名空间的名称作为前缀来从外部访问,最常见的是std命名空间,使用标准头文件内的变量类型、函数算法等必须要先使用std命名空间作用域,也可以使用namespace关键字自定义命名空间内的变量或函数,使用时遵循同样的添加作用域的使用方法。
- 类作用域:在类内部声明的成员变量和成员函数具有类作用域。成员变量和成员函数可以通过类的对象来访问,或者在某些情况下(如静态成员)可以通过类名直接访问,类会在(二)面向对象编程中讲解。
- 块作用域:这是局部作用域的一个特例,指的是由大括号{}包围的代码块内部声明的变量。这些变量只能在该代码块内被访问。
示例代码:
#include<iostream>
int global = 10; // 全局作用域变量
//namespace关键字定义自定义的命名空间mynamespace的命名空间作用域
namespace mynamespace {
//定义命名空间内的变量number
int number = 10;
//定义命名空间内的函数printf
void printf() {
std::cout << "mynamespace::printf函数调用!" << std::endl;
}
}
//定义类或结构体
class Myclass{
int class_number; // 定义Myclass类变量class_number
};
int main()
{
int local = 20; // 局部作用域变量
if(true) {
int block = 30; // 块内作用域变量block
}
mynamespace::number++; // 使用自定义命名空间作用域mynamespace变量number
mynamespace::printf(); // 使用自定义命名空间作用域mynamespace变量函数
// 使用标准命名空间std的cout和endl
std::cout << "hello world" << std::endl;
return 0;
}
6.extern关键字实现C++程序同时兼容C环境和C++环境
C语言源文件使用.c扩展名编译时仅支持C语法。.cpp扩展名的源文件则采用C++编译器,其语法通常向下兼容C语言。对于头文件,.h扩展名可同时用于C/C++,而.hpp扩展名专门用于C++头文件。
由于C++语法支持函数重载,对函数有名称修饰的作用,而C语言没有,因此在C++环境下直接使用.c后缀源文件的C函数按照C++语法编译会出现问题,为在C++环境下可以使用到C语言函数,可以使用extern关键字在C++环境进行显式声明.c后缀源文件的C函数为C函数,不进行名称修饰,而使在C++环境下可以直接使用C函数,同时为支持编写的C++程序同时可以兼容C环境和C++环境,即保证C++程序可以在C环境下运行,在编写的C++程序时不使用C++语法。
extern关键字
用于通过头文件展开等方式在C++源文件声明函数时,声明定义在.c后缀源文件中的C函数为C语言函数,以支持C++源文件.cpp中可以使用定义在C源文件.c中的C函数。extern使用方式示例代码:
// 在C++源文件中声明一个C函数
extern "C" int ave(int, int);
// 在C++源文件中声明一群C函数
extren "C" {
int ave(int, int);
int pve(int, int);
}
// 在C++源文件中声明一个头文件内的所有函数为C函数
extern "C" {
#include "c.h" // 包含所有C函数的c.c源文件的头文件
}
采用下述头文件声明方式,可以实现.c后缀的C函数源文件在C++环境或C环境下均可直接使用,采用.c后缀的源文件只能使用C语言语法,其中__cpiusplus宏在只在C++环境下被编译器定义,在C环境下不存在,从而实现在C++环境下使用extern关键字声明为C函数,在C环境下不使用。
// func.h
#ifdef __cplusplus
extern "C" {
#endif
#include "func.h" // 在C++环境下声明该头文件内的所有函数为C函数
int ave(int, int); // 在C++环境下声明该C语言函数为C函数
#ifdef __cplusplus
}
#endif
// func.c源文件中的C函数ave
int ave(int a, int b){
return a+b;
}
7.C++流式输入输出方式和C输入输出方式
7.1.C++流式输入输出方式
C++的流式输入输出(I/O)是编程的基础核心功能,实现了程序与外部环境(如用户交互、文件操作等)的数据交换。标准库通过<iostream>、<fstream>和<sstream>等头文件提供了完善的I/O支持。下面我们将系统讲解这些关键组件的核心概念和使用方法。
1. 标准输入输出流
iostream是标准输入输出库,为控制台(命令行/终端)操作提供了输入输出功能。主要包含以下常用类:std::cin(输入)、std::cout(输出),以及std::cerr(非缓冲错误输出)和std::clog(缓冲错误输出)。常用的标准输入输出对象包括以下:
std::cin:标准输入流对象(std::istream类型),从标准输入一般为键盘,将数据流式读取到指定变量中。std::cout:标准输出流对象(std::ostream类型),将指定变量以数据流式写入到标准输出中,一般指显示屏。- std::cerr:标准错误流对象(
std::ostream类型),无缓冲机制,立即输出数据到标准输出,一般指显示屏,常用于错误信息输出,采取立即输出方式。
常用函数
- 输入:
std::cin >> variable;:读取数据到变量,跳过空白字符。std::cin.get(char& c);:读取单个字符(包括空白字符)。std::cin.getline(char* buffer, size);:读取一行到字符数组。std::getline(std::cin, std::string& str);:读取一行到std::string。
- 输出:
std::cout << variable;:输出变量值。std::cout << std::endl;:输出换行并刷新缓冲区。
格式化输出流方式:

示例代码
#include <iostream>
#include <string>
int main() {
int num;
std::string name;
std::cout << "Enter a number: ";
std::cin >> num; // 读取整数
std::cout << "Enter your name: ";
std::cin.ignore(); // 忽略前一个输入的换行符
std::getline(std::cin, name); // 读取整行
std::cout << "Number: " << num << "\nName: " << name << std::endl;
return 0;
}
2. 字符串输入输出流
字符串流库sstream允许你以流的方式对字符串进行读写操作,包括std::istringstream(输入字符串流)、std::ostringstream(输出字符串流)和std::stringstream(同时支持输入输出的字符串流),没有直接提供的std::stringstream、std::istringstream、std::ostringstream这3个流对象,需要自定义,分别在相应的<sstream>、<isstream>、<osstream>头文件中。
std::stringstream ss:提供内存中的字符串流操作,支持直接通过流方式读取或写入数据。- std::istringstream iss:提供内存中的字符串流操作,只支持通过数据流方式将指定变量数据读取到字符串流中。
- std::ostringstream oss:提供内存中的字符串流操作,只支持通过数据流方式将字符串流中数据进行输出到指定变量中,其中以换行来分割不同数据,接收变量类型需要相应匹配。
常用函数
- 输入:
ss >> variable;:从字符串流提取数据到变量。oss >> variable;:从字符串流提取数据到变量。
- 输出:
ss << variable;:将变量插入到字符串流。iss << variable;:将变量插入到字符串流。
- 管理内容:
std::string str = ss.str();:获取字符串流内容转换为字符串。ss.str("new content");:重置流内容为新字符串"new content"。
示例代码
#include <sstream>
#include <iostream>
int main() {
std::stringstream ss;
// 写入字符串流
ss << "PI = " << 3.14 << ", Count = " << 10;
// 读取字符串流
double pi;
int count;
std::string token;
ss >> token >> token >> pi >> token >> token >> count;
std::cout << "Extracted: PI=" << pi << ", Count=" << count << std::endl;
return 0;
}
3. 文件输入输出流
文件输入输出库fstream同样允许通过流方式进行文件数据的读取和写入。同样包括3种类:std::ifstream(输入文件流)、std::ofstream(输出文件流)和std::fstream(同时支持输入输出的文件流)三个类。同样不像标准输入输出流会直接提供类似于std::cout等对象,需要类似于字符串流自定义文件输入输出流对象,并且文件操作需要先打开文件,读取或写入文件,最后关闭文件。包括如下定义文件输入输出流对象:
std::ifstream ifs:输入文件流(读文件),只能从文件中以流方式读取数据到变量。std::ofstream ofs:输出文件流(写文件),只能将变量以流方式写入数据到文件。std::fstream fs:双向文件流(读写文件),同时支持以文件流方式读取或写入数据。- 通过打开模式(如
std::ios::app)控制文件行为。
常用函数
- 打开/关闭文件:
file.open("path", mode);:打开文件,mode包括以下方式打开文件,并允许使用"|"符号进行组合打开方式。- std::ios::in 以只读模式打开文件
- std::ios::out 以写入模式打开文件
- std::ios::app 以追加模式打开文件
- std::ios::ate 打开文件后,文件指针定位到文件的末尾
- std::ios::binary 以二进制模式打开文件,用于读写二进制数据
- std::ios::noreplace 如果文件已存在,打开操作将失败。
file.close();:关闭文件。
- 状态检查:
file.is_open();:检查是否成功打开。file.eof();:检查是否到达文件末尾。
- 读写操作:
file >> variable;: 以文件流方式将文件读取文件数据到variable中file << variable;:以文件流方式将variable写入到文件中std::getline(file, str);:从文件中读取一行数据到字符串str中file.read(buffer, size);: 从文件中读取size个字符到buffer中file.write(buffer, size);:将buffer中size个字符写入文件中
- 文件指针:
file.seekg(offset, way);:设置读指针位置,offset表示在way基础上的偏移量。file.seekp(offset, way);:设置写指针位置,way表示文件位置。- std::ios_base::beg; 参考点为文件开头。
- std::ios_base::cur; 参考点为文件指针的当前位置。
- std::ios_base::end; 参考点为文件末尾。
示例代码
#include <fstream>
#include <iostream>
int main() {
// 1.将数据写入文件示例
// 创建写入文件流对象,并通过写入+追加方式打开文件
std::ofstream ofstream("./test.txt", std::ios::out | std::ios::app);
if (!ofstream.is_open())
std::cout << "文件./test.txt打开失败!" << std::endl;
// 向文件中写入数据
ofstream << "http::www.baidu.com\r\n";//向文件写入一行字符串\r\n表示回车换行
ofstream << "hello baidu!\r\n" << "Connection: close\r\n\r\n";//允许链式操作
// 关闭文件
ofstream.close();
// 2.从文件读取数据示例
// 创建读取文件流对象,并通过只读方式打开文件
std::ifstream ifstream("./test.txt", std::ios::in);//打开文件
if (!ifstream.is_open())
std::cout << "文件./test.txt打开失败!" << std::endl;
// 读取文件数据到字符串st中
std::string st;
//循环一行行读取文件内容,根据读取函数返回的流对象本身判断是否读取到文件末尾
while (std::getline(ifstream, st))
std::cout << st << std::endl;
// 关闭文件
ifstream.close();
return 0;
}
7.2.C格式化输入输出方式
1.格式化输出函数
包括以下几种格式化输出方式:
- printf函数:格式化输出到标准输出设备,一般指屏幕,将指定数据进行相应%格式替换,其他原封不动输出,支持vprintf不定量参数函数。
# 格式化输出数据到标准输出设备上
int printf(const char *format, ...);
# 支持不定量参数版本的printf
int vprintf(constchar* format,va_list arg);
- sprintf函数:格式化输出到C风格字符串数组,同时存在相对应支持va_list不定量参数的vsprintf函数。
- snprintf函数:格式化输出到C风格字符串数组,并指定最大长度,防止缓冲区溢出,同时存在相对应支持va_list不定量参数的vsnprintf函数。
# 格式化输出到指定C风格字符串
int sprintf(char *str, const char *format, ...);
# 显示指定C风格字符串最大接收长度,避免溢出
int snprintf(char *str, size_t size, const char *format, ...);
- fprintf函数:格式化输出到指定文件中,同时存在相对应支持va_list不定量参数的vfprintf函数。
# 格式化输出数据至文件
int fprintf(FILE *stream, const char *format, ...);
# 支持不定量参数版本
int vfprintf ( FILE * stream, const char * format, va_list arg );
示例代码:
// 输出格式化后的字符串到标准输出,一般为显示屏
char str[1024] = "Bob";
int age = 18;
double grade = 100;
printf("%s, age=%d, grade=%lf\n", str, age, grade);
char buffer[64];
// 向缓冲区写入格式化输出内容
sprintf(buffer, "%s, age=%d, grade=%f\n", "Hello World!", 18, 99.5);
std::cout << buffer << std::endl;
// 向缓冲区写入格式化输出内容,指定最大长度
snprintf(buffer, sizeof(buffer), "%s, age=%d, grade=%f\n", "Hello World!", 18, 99.5);
std::cout << buffer << std::endl;
// 格式化输出内容到文件中
// 以读写方式打开当前路径下的指定文件,如果指定文件不存在则创建并以读写方式打开
FILE* file = fopen("fprintf.txt", "w+");
if (file) {
/// 向文件中写入格式化输出内容
fprintf(file, "%s, age=%d, grade=%f\n", "Hello World!", 18, 99.5);
char buffer[1024];
// 读取文件内容
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
// 从文件中读取内容到缓冲区
size_t bytesRead = fread(buffer, 1, sizeof(buffer) - 1, file);
buffer[bytesRead] = '\0'; // 添加字符串结束符
std::cout << "fprintf.txt: " << buffer << std::endl;
fclose(file); // 关闭文件
}
else
std::cerr << "Failed to open file." << std::endl;
采用v开头的参数列表方式的格式化输出方式:
#include <cstdarg> // 不定量参数
// 不定量参数形式的格式化输入函数示例
void v_printf(const char* format, ...) {
// 先定义一个va_list类型的变量,用于存储不定量参数列表
va_list args;
// va_start函数初始化args变量,使其指向第一个不定量参数,format用来表示第一个不定量参数的位置在最后一个固定参数format之后
va_start(args, format);
// 使用不定量参数printf函数进行格式化输出到字符串中
vprintf(format, args);
// va_start函数初始化args变量,使其指向第一个不定量参数,format用来表示第一个不定量参数的位置在最后一个固定参数format之后;
char buffer[1024];
// 使用不定量参数printf函数进行格式化输出
va_start(args, format);
vsprintf(buffer, format, args);
std::cout << buffer << std::endl;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
std::cout << buffer << std::endl;
// va_start函数初始化args变量,使其指向第一个不定量参数,format用来表示第一个不定量参数的位置在最后一个固定参数format之后
FILE* file = fopen("vfprintf.txt", "w+");
if (file) {
// 使用不定量参数printf函数进行格式化输出到文件中
va_start(args, format);
vfprintf(file, format, args);
fseek(file, 0, SEEK_SET); // 将文件指针移动到文件开头
char buffer[1024];
int readBytes = fread(buffer, 1, sizeof(buffer) - 1, file); // 从文件中读取内容到缓冲区
fclose(file);
buffer[readBytes] = '\0'; // 添加字符串结束符
std::cout << "vfprintf.txt: " << buffer << std::endl;
}
}
2.格式化输入函数
与printf格式化输出函数相对应,同样包括以下几种格式化输入函数
- scanf函数:格式化从标准输入设备读取数据,从标准设备输入时默认空格分隔,当格式化字符串有除%格式的其他字符时,输入时也需要输入相应字符进行匹配,支持vscanf不定量参数函数。
# 格式化字符串从标准设备输入
int scanf(const char *format, ...);
- sscanf函数:格式化从C风格字符串数组读取数据,同时存在相对应支持va_list不定量参数的vsscanf函数。
# 从字符串缓冲区格式化字符串输入
int sscanf(const char *str, const char *format, ...);
# 支持参数列表方式
int vsscanf ( const char * s, const char * format, va_list arg );
- fscanf函数:格式化从指定文件中读取数据,同时存在相对应支持va_list不定量参数的vfscanf函数
# 从文件中格式化输入
int vfscanf ( FILE * stream, const char * format, va_list arg );
# 同样有支持参数列表方式
int fscanf(FILE *stream, const char *format, ...);
格式化输入输出字符表

注意在这些格式化输入输出字符前使用整形数字可以限制输入输出的该数据宽度
# 表示按照4字节宽度输出x到标准输出中,同样适用于sprintf
printf("%4d",&x);
# 表示从标准输入中最多接收32字节的字符数据存储在str中,同样适用于sscanf
char str[32];
scanf("%32s",str);
%[]自定义扫描集实现自定义匹配规则进行输入scanf系列函数,注意无法用在输出printf系列函数
- 正向匹配规则(只匹配在[]内的字符)
# 表示从标准输入中只匹配a\b\c这三种字符进行保存到s中 scanf("%[abc]", s); # 345abc123 -> abc # 表示从标准输入中只匹配a-z\A-Z\0-9这三个区间内字符进行保存到s中 scanf("%[a-zA-Z0-9]", s); # ~ddf123$32& -> ddf12332 -
反向匹配规则(匹配直到出现[^]内的任一字符为止
# 表示从标准输入中接收直到出现\n或&为止的所有字符保存到s中 scanf("%[^&\n]",s); # dygdwy\n -> dygdwy
“*”号字符结合格式化输入输出的匹配含义
- 在scanf等格式化输入字符串中,在格式化字符前加上"*"表示只进行该类型数据匹配,不进行存储变量中,并且这一规则同样适用于%[]中自定义扫描集。
# 当在标准输入“123 456”时,首先会先匹配到整形123,但是采用"*"字符表示只进行匹配,变量x不会进行存储,匹配到整形456时会存储在变量x中 scanf("%*d %d", &x); # 忽略第一个数,读第二个scanf("%*[^=]=%99[^&\n]", value);在这个自定义扫描集中的匹配规则:第一个自定义扫描集%*[^=]会匹配=前面的所有非=号的字符,但使用"*"表示仅进行匹配不存储,继续匹配=,然后第二个自定义扫描集%99[^&\n]会匹配所有非&\n前面的所有字符,并且最多匹配99个字符存储在value中保存。
- 在printf等格式化输出字符串中,表示指定该类型数据的输出宽度为提供的数字。
# 按照5字节宽度输出整形123 printf("%*d\n", 5, 123); # 等价于下面 printf("%5d\n", 123);
8.指针
C++指针和C语言指针的基本概念和操作方式相同,指针用于存储变量的内存地址,使用&去地址运算符可以获取变量地址,使用*取值运算符可以直接访问该地址变量,指针也允许进行+、-运算,其改变的是存储的地址,并且是通过变量字节个数大小进行改变。
常量指针和指针常量:const与*位置不同修饰指针所指变量*p或指针p为常量,另一个p或*p相应为变量可变。
- 常量指针:const关键字在*符号前面,表示修饰的指针无法通过指针方式的取值运算*来改变变量的值,const void* 。
- 指针常量:*符号在const关键字前面,表示const修饰指针为常量,即指针的值即存储的变量地址不可改变,该指针只能指向该变量并无法改变,void* const。
- 常量指针常量:同时具备上述两种性质,即无法通过指针的*取值运算符修改所指向变量的值,也无法改变指针所存储的变量地址值,const void* const。
常量必须使用常量指针或常量引用接收,以防止通过指针或引用方式修改常量,编译器不允许这种操作。
示例代码:
int aa = 10, bb = 20, cc = 30;
const int* paa = &aa;//直接翻译成中文:常量指针,const修饰*paa,通过paa指针所指*paa不可修改,但可以通过aa变量来修改,指针paa指向可变
int* const pbb = &bb;//直接翻译成中文:指针常量,const修饰pbb,指针pbb指向为常量所指位置不可变,通过paa指针所指*pbb可以修改
const int* const pcc = &cc;//直接翻译成中文:常量指针常量,通过*pcc修改变量cc和指针pcc指向都不可变
//aa = 30; paa = &cc; bb = 30; *pbb = 30;cc = 100;正确 //*paa = 30;pbb = &cc; pcc = &aa; *pcc = 100; 错误
void*表示为万能指针,由于指针本质上是存储变量地址,地址位数和内存寻址范围相关,因此所有变量地址的位数均相同,一个指针本质上可以表示任意地址,void*就是表示任意类型变量的地址,其可以接收任意类型变量地址,但在使用时必须先强制转化为指定类型地址,然后才可以使用。
示例代码:
int number;
void* vptr = &number; //保存int类型内存地址
//*vptr = 100; //void*类型指针不能直接使用
int* pnumber = (int*)vptr; //强制类型转换为int*类型才可以进行使用
*pnumber = 100;
空指针NULL和nullptr
在C语言中使用NULL表示空指针,其在本质上是一个值为0的宏,在C++使用其表示空指针时,在某些情况下会出现隐式类型转换的错误,因为其本质使用整型0,C++使用关键字nullptr表示空指针,并且不会出现隐式类型转换为整型,只允许隐式类型转化为地址,保证程序的安全性。
9.C风格字符串和C++的std::string字符串类
9.1 C风格字符串
C标准字符串本质上是一个以'\0'结尾的char类型数组即char[]类型,其等价于char*指针,并且字符数组和堆区char数组指针这两者均允许接收和表示一个字符串,并且C++常用的std::string字符串类本质上也是采用C标准字符串实现的。
# 数组表示字符串
char str[] = {'H','e','l','l','o','\0'}; // 字符串以'\0'字符结尾
char str[] = "Hello"; // ""表示字符串,自动在末尾添加'\0'
char* pstr = str; // 数组名等价于指针
# 指针表示字符串
char* pstr = new char[6]("Hello"); // 管理一块堆区内存的指针表示字符串
const char* pstr = "Hello"; // ""表示字符串,自动在末尾添加'\0',并且值为常量指针
值得注意的是字符串常量的本质是const char*类型的指针,即常量字符指针,其指向字符串常量的第一个字符地址。
C风格字符串处理函数
常用于处理以\0结尾的C风格字符串的函数,主要可以分为以下几类:
- 字符串长度计算函数
计算字符串的实际长度(不包括空字符\0)
# 获取字符串str的字节数
int strlen(const char* str);
-
字符串复制函数
将字符串src复制数据到字符串dest中,包括以下3种函数:
# 将源字符串 src(包括终止空字符 \0)复制到目标缓冲区 dest,返回目标字符串的指针
char *strcpy(char *dest, const char *src);
# 从源字符串 src复制最多 n个字符到目标缓冲区,不会自动添加\0
char *strncpy(char *dest, const char *src, size_t n);
# 需要明确指定目标缓冲区大小 destsz,并自动添加\0
errno_t strcpy_s(char *dest, rsize_t destsz, const char *src);
strcpy函数不会进行安全边界检查,在src源字符串长大于dest目标字符串长时,会出现dest缓冲区溢出的情况,非常不安全。
strncpy函数通过增加一个参数n来指定dest缓冲区最大接收长度实现安全的边界检查,注意在最后不会为dest缓冲区添加\0,需要手动添加。
strcpy_s函数在第二个参数destsz来指定dest缓冲区长度,保证复制拷贝操作不会溢出,并在最后会自动为dest缓冲区添加\0结束符。
- 字符串拼接函数
将两个字符串进行首尾拼接,包括以下3种函数:
# 将src字符串拼接到dest字符串尾部
char *strcat(char *dest, const char *src);
# 将源字符串src的最多前 n个字符连接到目标字符串dest
char *strncat(char *dest, const char *src, size_t n);
# 将源字符串src拼接到dest字符串尾部,并进行dest缓冲区边界检查
errno_t strcat_s(char *dest, rsize_t destsz, const char *src);
strcat函数不会进行安全边界检查,直接将src字符串拷贝拼接到dest字符串尾部,并在最后自动添加\0,在dest缓冲区过小时,会出现dest缓冲区溢出的情况,非常不安全。
strncat函数通过增加一个参数n来指定dest缓冲区剩余最大接收长度max实现安全的边界检查,注意在最后会为dest缓冲区添加\0,因此实际dest可接收的字符串串长应该是max-1。
strcat_s函数在第二个参数destsz来指定dest缓冲区长度,保证拼接操作不会溢出,并在最后会自动为dest缓冲区添加\0结束符。
- 字符串比较函数
将两个字符串进行比较,按照ASCII码进行逐字符比较。
# 比较str1和str2逐字符进行比较
int strcmp(const char *str1, const char *str2);
# 比较str1和str2的前n个字符,逐字符进行比较
int strncmp(const char *str1, const char *str2, size_t n);
strcmp函数会逐字符按照ASCII码进行比较两个字符串,直到遇到\0终止符或出现逐字符比较的ASCII码不相等时,提前终止。
strncpy函数只会比较指定的两个字符串前n个字符,也是直到遇到\0终止符或出现逐字符比较的ASCII码不相等时,提前终止。
- 字符串数据转换函数
将字符串类型数据进行整形或浮点型数据间的相互转化。包括以下几种函数:
#include<stdio.h>
# atox系列,默认采用10进制方式
# 将字符串转换成浮点型数
double atof(const char*);
# 将字符串转换成整型数
int atoi(const char*);
# 将字符串转换成长整型数
long int atol(const char*);
# strtoX系列:可以指定base进制,endptr一般为空nullptr
# 将字符串转换成整数
long int strtol(constchar*str,char**endptr,intbase);
# 将字符串转换成无符号整数
unsigned long int strtoul(constchar* str,char** endptr,,int base);
# 将字符串转换成双精度数
double strtod (const char* str, char** endptr);
上述函数包括atoX系列、strtoX系列,均是将字符串类型转换为相应整形或浮点数类型,将整型或浮点数类型转化为字符串可以使用下面gcvt函数。
# 将浮点型数/整数转换为字符串
char *gcvt(double value, int ndigit, char *buf);
在gcvt函数中,ndigit 指的是最大有效位数比实际value数值位数小时保留的实际有效位数,否则保留全部value有效位数,buf参数和返回值的结果相同均表示字符串。
示例代码
//C风格字符串及其操作函数,<cstring>头文件是C语言头文件<string.h>的C++版本
char s1[] = "hello world!";//C风格的字符串,原理为字符数组str1[],自动添加空字符'\0'结尾表示C风格字符串结束
char s2[] = "Hello World!";//C风格的字符串
//C风格字符串操作函数,操作的字符数组必须以空字符'\0'结尾才表示字符串结束,并且也是字符串操作函数判定字符串结束标志
//strlen(s);//返回字符串s的长度,空字符'\0'不计算在内
cout << "C风格字符串s1的长度(不包括空字符'\\0'):" << strlen(s1) << endl;
//strcmp(s1,s2);//比较s1和s2的是否相等,如果相等返回0,如果s1>s2返回一个正值,如果s1<s2返回一个负值
cout << "C风格字符串s1和s2比较:" << strcmp(s1, s2) << endl;
//strcat(s1,s2);将s2附加到s1之后,返回s1,s1字符数组必须有足够大的空间,C++支持更安全的strcat_s(s1,sizeof(s1),s2);
//strcpy(s1,s2);将s2拷贝给s1,返回s1,s1字符数组必须有足够大的空间,C++支持更安全的strcpy_s(s1,sizeof(s1),s2);
char* S = (char*)malloc((strlen(s1) + strlen(s2) + 1) * sizeof(char));//重新在堆区开辟一块足够大的空间用于保存s1+s2,+1是为了保存空字符'\0'
S[0] = '\0';//给字符数组S添加空字符'\0',将字符数组变成一个字符串,以便可以使用字符串操作函数
strcpy_s(S, strlen(s1) + strlen(s2) + 1, s1);//strcpy(S,s1);//字符串S复制字符串s1
strcat_s(S, strlen(s1) + strlen(s2) + 1, s2);//strcat_s(S,s2);//字符串s2拼接到字符串S后面
cout << "s1和s2字符串拼接后的字符串S为:" << S << endl;
free(S);//使用完后释放堆区内存
//C++支持C风格字符串,但在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,
// 是诸多安全问题的根本原因,一般在C++程序中使用string类型的字符串。
string str1 = "hello world!";//C++的string类的字符串类型
cout << "C++的string类型的字符串str:" << str1 << endl;
9.2 C++的std::string字符串类
C++的标准字符串类std::string是标准模板库(STL)中定义的一个类,位于<string>头文件中。它提供了一系列强大的功能来处理和管理字符串数据,相比C风格的字符数组(char*)更加安全和方便,其本质也是基于C风格字符串实现封装的,在接下来的(三)泛化编程会详细说明。
主要特性包括:
- 动态内存管理:自动处理内存分配和释放,无需手动管理
- 丰富的成员函数:
- 构造和赋值:
assign(),operator= - 容量操作:
size(),length(),capacity(),reserve() - 元素访问:
operator[],at(),front(),back() - 修改操作:
append(),insert(),erase(),replace() - 字符串操作:
substr(),compare(),find()
- 构造和赋值:
- 运算符重载:支持
+,+=,==,!=等运算符 - 迭代器支持:可以使用STL算法处理字符串
常用操作示例:
#include <string>
#include <iostream>
int main() {
std::string s1 = "Hello"; // 初始化
std::string s2(" World"); // 另一种初始化方式
// 字符串连接
std::string s3 = s1 + s2;
std::cout << s3 << std::endl; // 输出: Hello World
// 查找和替换
size_t pos = s3.find("World");
if(pos != std::string::npos) {
s3.replace(pos, 5, "C++");
}
std::cout << s3 << std::endl; // 输出: Hello C++
// 子字符串
std::string sub = s3.substr(6, 3);
std::cout << sub << std::endl; // 输出: C++
return 0;
}
std::string是C++中最常用的字符串处理工具,相比C风格字符串更安全、更方便,是大多数应用的首选。
10.左值引用
左值引用(通常简称为引用)是为变量内存创建别名的机制。通过&符号声明引用时,原变量名和别名实际指向同一块内存空间,语法为:数据类型 &别名变量名=原变量名。
引用具有以下特性:
- 必须在定义时初始化,且必须绑定到合法的内存地址
- 绑定后不可更改指向的对象
- 常用于函数参数传递、返回值返回和类成员变量声明,实现对原始数据的直接访问,既提升程序执行效率又增强代码可读性
引用在本质上是一个指针常量,所以引用必须初始化绑定合法内存地址,并且绑定后不可更改指向的对象,还可以通过引用改变原变量内存值。
常量引用:本质上是常量指针常量,相比引用还限定无法通过别名来修改内存值,但对于使用原名修改内存值无限制。
示例代码:
//形参为引用方式实现打印,实参内存可能在函数中通过引用形参修改
void yinyongprinta_2(int& a_2) {
a_2 = 200;
cout << "形参为引用方式可以修改后的\ta_2=" << a_2 << endl;
}
//形参为常量引用方式实现打印,实参内存在函数中无法通过常量引用形参修改
void constyinyongprinta_2(const int& b_2) {
//b_2 = 200;报错常量引用形参不可修改
cout << "形参为常量引用方式不可修改后的\tb_2=" << b_2 << endl;
}
int a_2 = 100, b_2 = 100, c_3 = 1000;
const int& c_3_1 = c_3;
c_3 = 10; //c_3_1 = 10;报错,通过原名可以修改,通过常量引用的别名无法修改会报错
cout << "通过常量引用的别名无法修改会报错,通过原名可以修改后c_3=" << c_3_1 << endl;
cout << "引用方式函数使用前\ta_2=" << a_2 << endl;
yinyongprinta_2(a_2);//函数形参为引用形式接收实参a_2,在函数内可能通过引用形参修改实参内存值
cout << "常量引用方式函数使用前\tb_2=" << b_2 << endl;
constyinyongprinta_2(b_2);//函数形参为常量引用形式接收实参b_2,在函数内无法通过引用常量形参修改实参内存值
11.多维数组、指针和引用
数组本质为一块连续的按照数据类型大小分块的内存空间,数组名为指向数组第一个元素的常量指针,因此指向数组第一个元素的指针,其等价于数组名,在本质上数组等价于表示一块连续堆区内存的指针。因此对于指针和数组名均提供两种访问连续内存的数组元素机制:通过[]数组下标访问、通过指针移动进行访问。该方式对于多维数组也同样适用,因为多维数组的底层存储方式和数组的连续存放方式相同。
一维数组的类型为T[b]的类型,采用指针和引用方式表示如下,注意区分指针数组和数组指针。
int array[10];
// 数组指针,声明array_ptr是一个表示int[10]数组的数组指针,注意不能直接使用array,其表示数组第一个元素地址
int (*array_ptr)[10] = &array; // 指针指向数组,数组类型为int[10],因此其指针类型为int[10]*
int* ptr_arrays[10]; // 指针数组,ptr_arrays表示一个int*[10]类型的数组
// 数组引用,声明array_ref是一个表示int[10]数组的数组引用
int (&array_ref)[10] = array; // 引用表示数组,数组类型为int[10],因此其引用类型为int[10]&
int& array_ref[10] = array; // 报错,编译器无法识别是数组还是引用
// 该指针等价于数组名,在编译器实现中数组即指针,可以通过[]数组下标和指针移动进行访问数组元素
int* parray = array; // 类型即数组第一个元素指针类型int*
for(int i=0; i<10; i++)
parray[i]==*(parray+i)==array[i]==*(array+i) // 在编译器底层实现时,数组即指针,指针即数组
二维数组的类型为T[a][b],在编译器底层实现中,其等价于a个T[b]类型的一维数组元素所构成的一维数组。采用指针和引用方式表示如下。
int arrays[10][20];
// 指针指向二维数组,类似于一维数组,用的不多
int (*arrays_ptr)[10][20] = &arrays;// 二维数组为int[10][20],其指针类型为int[10][20]*
int* arrays_ptr[10][20]; // int[10][20]的二维指针数组
// 引用表示二维数组
int (&arrays_ptr)[10][20] = arrays;// 二维数组为int[10][20],其引用类型为int[10][20]&
int& arrays[10][20]; // 报错,编译器无法识别是二维数组或引用
// 该指针等价于二维数组名
int (*parrays)[20] = arrays;// 类型即二维数组第一个元素指针类型int[20]*
for(int i=0;i<10;i++)
for(int j=0;j<20;j++)
arrays[i][j]==*(arrays+i*20+j)==parrays[i][j]==*(parrays+i*20+j)
12.结构体struct、联合体union、枚举enum
12.1 enum枚举类型的定义和使用。
C风格的枚举:定义时只需使用关键字enum即可,不需要携带作用域使用枚举值,不同枚举类型的枚举值相互暴露,类型强制为int类型并允许进行隐式转换为整形。
//使用enum关键字以类似于结构体方式定义枚举类型,注意使用逗号分隔
enum Color {
Red = 0,
Green = 10,
Yellow = 100,
Black = 1000
};
//创建枚举类型变量,只能使用枚举值进行初始化
Color color = Color::Red;
//类似于普通变量一样使用枚举型变量,但只能使用枚举值,实际使用时,在底层会被解析为对应的int类型值,也可以直接使用枚举值类似于宏
color = Color::Green;
cout << "Green枚举值对应的整形值为:" << color << endl << "Red枚举值对应的整形值为:" << Color::Red << endl;
C++11以上的强类型枚举:定义时必须使用关键字enum class/struct,使用时必须携带枚举值作用域::,不允许隐式转换为其他类型而必须显示转换,不使用默认的int时可以指定其他整形类型。
enum class EquipLv:short
{
normal=10,armyA=normal,
high, armyB = high,
rare, armyC = rare,
epic, armyD = epic,
legend, armyE = legend,
myth=100, armyS = myth
};
EquipLv weaponCLv = EquipLv::normal;
EquipLv weaponDLv = EquipLv::myth;
short diff = (int)weaponDLv - (int)weaponCLv;
std::cout << "这两件装备的等级差" << diff;
要点列表:
-
枚举类型可以提高代码的可读性和安全性
-
枚举类型默认是int类型
-
枚举类型成员只能是整数类型
-
枚举类型和其他类型转换需要强制转换
-
在没有显式赋值的默认情况下,枚举类型的下一个项的初始值是上一个项的初始值+1
12.2 union联合体的定义和使用
联合体(union)是一种用户定义的数据类型,它允许在相同的内存位置存储不同的数据类型。联合体中的所有成员共享同一块内存区域,在任何给定时间,联合体只能存储其成员中的一个值,再次使用会覆盖之前存储的值。
C风格联合体:在C语言中,联合体成员都是没有构造/析构函数、纯字节的平凡类型(常见的基本数据类型),可以直接复用该分配的内存空间储存数据,不需要使用前构造和使用后析构。
# 使用union关键字以类似于结构体方式定义联合体
union Data {
int age;
float sorce;
char name[10];
};
Data data = { 42 };//使用{}初始化,使用int类型成员
//以类似于结构体方式使用联合体成员
data.sorce = 95.5;
cout << "联合体union存储的数据为:" << data.sorce << endl;
C++11版本以上联合体:联合体成员除平凡类型外,还可能是有构造/析构函数的、堆内存的非平凡类型(STL容器,自定义类型)时,并且多个类型数据由于共享内存会带来以下几个问题。
- 由于这些非平凡类型联合体成员具有构造函数、析构函数、拷贝语义、移动语义,此时联合体必须要手动实现构造函数、析构函数、可能需要拷贝语义、移动语义。并且构造函数只能初始化一个联合体成员,析构函数在判断当前使用非平凡类型成员时,需要调用非平凡类型的析构函数,但此时通常无法判断当前使用的联合体成员类型,往往需要上层封装记录当前联合体使用的成员类型。
- 编译器由于不知道共享内存使用什么类型的联合体成员,不会自动为该非平凡类型调用构造函数、也不会在复用其他成员时调用析构函数,因此对于非平凡类型的联合体成员的使用需要手动管理生命周期,在使用前使用new调用构造函数,在切换时调用析构函数释放资源。
# C++11版本的联合体可能具备非平凡类型成员,此时需要手动实现构造/析构函数、手动管理生命周期
union Data {
int age;
float sorce;
std::string name; // 非平凡类型
// 构造函数,只能初始化一个联合体成员
Data():age(0){}
Data(const std::string& str){
new (&name) std::string(str);
}
Data(int num):age(num){}
// 析构函数,在判断为非平凡类型std::string,需要手动调用其析构函数释放资源
~Data(){ name.~basic_string(); }
};
Data data = { 42 }; // 调用构造函数Data(int)
// 复用使用std::string非平凡类型成员,需要先显示调用其构造函数初始化
new(&data.name)std::string("Bob");
cout << "联合体union存储的数据为:" << data.name << endl;
// 切换复用其他联合体成员时,需要手动调用该非平凡成员的析构函数
data.name.~basic_string();
data.sorce = 95.5;
在C++17及以上版本:具有非平凡类型的联合体成员时,可以使用std::variant<T1,T2,T3,...>类型安全联合体更加方便,其自动实现非平凡类型的构造/析构函数的生命周期管理、及联合体的构造函数、析构函数、拷贝语义、移动语义。
# C++17及以上版本使用std::variant类型安全的联合体
std::variant<int, float, std::string> data;
data = 42;
// 编译器未优化时存在临时对象
data = std::string("Bob"); // 切换非平凡类型std::string自动管理构造和析构
# emplace<T>成员函数直接在内部调用T类型相应构造函数存放,不存在临时对象
data.emplace<std::string>("Bob"); // 直接在data调用std::string的构造函数
data = 95.5;
# 使用std::get<T>函数获取指定T类型的数据,当std::variant不是存储T类型时抛出异常
std::string name = std::get<std::string>(data); // 异常,data最后储存float类型
float sorce = std::get<float>(data); // 正常
# 使用std::get_if<T>函数在类型正确时,获取T类型非空指针T*
std::string* pname = std::get_if<std::string>(data);
if(pname) cout << *pname << endl;
12.3 struct结构体的定义和使用
在C/C++语言中,struct(结构体)是一种用户自定义的数据类型,它允许将不同类型的数据组合成一个整体。结构体常用于表示具有多个属性的实体。在C语言中,结构体仅作为数据封装的基本单元;而在C++中,结构体则具备了与类相似的功能特性。其定义和使用方式如下:
// 使用struct进行结构体类型定义
struct student {
string name;
int age;
int score;
}stu3;//结构体第三种定义变量类型方式
struct teacher {
string name;
int id;
int age;
string zhicheng;
struct student stu[2];//嵌套结构体
};
student stu1;//结构体第一种定义变量方式,C++可以实现结构体变量名就可,C中必须要有struct关键字
struct student stu2 = { "赵四",14,60 };//结构体第二种定义变量方式并且能够使用{}直接赋值初始化
struct student stu3; stu3 = { "Jerry",21,60 };//结构体允许使用{}对创建好的结构体变量直接赋值
//直接访问:结构体变量本身直接通过 . 来实现对变量属性的使用
stu1.name = "王五"; stu1.age = 15; stu1.score = 100;
cout << "姓名=" << stu1.name << " 年龄=" << stu1.age << " 分数=" << stu1.score << endl;
cout << "姓名=" << stu2.name << " 年龄=" << stu2.age << " 分数=" << stu2.score << endl;
cout << "姓名=" << stu3.name << " 年龄=" << stu3.age << " 分数=" << stu3.score << endl;
// 结构体数组的使用
struct teacher teach[4] = { {"王老师",01,50,"教授",{{"张三",23,80},{ "关羽",45,200 }}},{"张老师",02,52,"院长",{{"陈八",24,90},{"赵灵儿",20,120}} },{"李老师",03,35,"讲师",{{"刘九",22,100},{"岳飞",36,150}}},{"唐老师",04,33,"助理",{{"赵六",23,85},{"刘备",46,130}}} };
cout << "嵌套结构体数组通过双重循环实现访问打印导师和学生名单及成绩" << endl;
for (int i = 0; i < 4; i++) {
cout << "姓名:" << teach[i].name << "\t编号:" << teach[i].id << "\t年龄:" << teach[i].age << "\t职称:" << teach[i].zhicheng << endl;
for (int j = 0; j < 2; j++)//循环输出teach[i]老师的student结构体数组的属性
cout << " 所带学生:" << teach[i].stu[j].name << "\t年龄:" << teach[i].stu[j].age << "\t分数:" << teach[i].stu[j].score << endl;
cout << endl;
}
13.C++函数进阶
13.1 函数形参传递方式
外部调用函数,包括两种传递参数方式:值传递和地址传递(指针传递和引用传递)。采用地址传递方式比值传递方式效率更高,特别是在传递大量数据的情况下。
- 值传递在函数内部会先完整拷贝副本数据到形参中,然后在函数体内使用,与函数外部的实参是两个独立变量。
- 地址传递包括:指针传递和引用传递,两者本质上是形参拷贝一份实参的地址,函数体内的形参会通过地址共享使用函数体外的实参变量,前者是指针,后者引用时指针常量。为防止在函数体内修改实参,地址传递方式通常使用
const关键字修饰指针和引用,即常量指针和常量引用(常量指针常量),以实现在函数体内无法通过指针地址方式修改实参数据,只允许进行读取。
特别的,对于数组类型在函数中进行传递,由于数组本质上是指针,并且防止拷贝整个数组以提高效率,通常进行地址传递方式,即采取指针传递方式或引用传递方式,仅传递数组首地址,包括以下两种等价的传递指针方式。
对于一维数组的地址传递方式,可以有以下几种等价方式
void Sort(int* ary, unsigned int arry_num); // 传递数组首地址和数组元素个数
void Sort(int ary[], unsigned int arry_num); // 传递数组首地址和数组元素个数
void Sort(int (*ary)[10]); // int (*ary)[10]的指针传递方式
void Sort(int (&ary)[10]); // int (&ary)[10]的引用传递方式
同理对于二维数组的地址传递方式,也类似的具有以下几种等价方式
void Sort(int ary[][2], unsigned count); // int ary[][2]的指针传递方式
void Sort(int (*ary)[2], unsigned count); // int (*ary)[2]的指针传递方式
void Sort(int (*ary)[10][20]); // int (*ary)[10][20]的指针传递方式
void Sort(int (&ary)[10][20]); // int (&ary)[10][20]的引用传递方式
13.2 函数返回值方式
函数的返回方式包括:值返回方式和地址返回方式(指针返回方式和引用返回方式),根据函数返回值类型进行区分,通常采用地址返回方式效率更高,需要保证传出的地址内存是有效的,尤其是在数组等数据大的类型。
- 值返回方式是返回函数内部一个变量的临时副本,即本质上发生了拷贝。
- 地址返回方式包括指针方式和引用方式,两者本质上是返回函数内部一个变量的地址。由于函数体内部的栈区变量在函数运行结束后会立即回收,通常采用地址返回方式的函数内部变量不能是函数体内部的局部栈区变量。
引用方式传递和返回的几个易错点:
-
指针返回允许返回一个空指针,但对于外部引用初始化的引用返回不允许引用传递一个空对象会出错。
-
值传递和返回允许发生隐式类型转换,但引用传递和返回,由于采用原变量不允许发生隐式类型转换会出错。
-
数组本质上是指针表示,引用方式也可以引用数组,但必须遵循正确的写法以方便编译器进行准确识别。
示例代码
# 值传递方式
int add(int a, int b) {
int sum = a + b;
return sum; // 返回int类型的临时副本
}
# 引用传递方式
int& add(int a, int b) {
static int sum = a + b;
return sum; // 返回int类型引用,static静态变量保证内存有效
}
# 指针传递方式
int* add(int a, int b) {
int* sum = (int*)malloc(sizeof(int));
*sum = a + b;
return sum; // 返回int类型指针,malloc动态内存分配保证内存有效
}
13.3 函数的不定量参数方式
main函数本质上也是一个函数,内部也可以在调用时接收外部参数,也叫命令行参数,然后在函数内部使用,其通过以下两种方式实现:采取一个整形接收命令行参数个数,一个字符指针数组来接收命令行参数字符串,每个命令行参数都被当成一个以空格相互隔开的字符串,其中argcount参数自动由实际输入的命令函数参数数量获得(包括默认输入执行的程序路径)而不需要在命令行参数中指定命令行参数个数,c_arg本质是存放char*类型数组,从其中可以获取输入的每个命令行参数字符串,其中默认是以输入执行的程序路径为开头c_arg[0],以nullptr作为结尾。

类似于main函数的方式来定义可以接收不定量参数的函数,通过在函数定义时,指定第一个参数为不定量参数的起始参数,第二个参数为...表示的不定量参数,并且这些不定量参数类型和第一个参数类型相同。在函数体中,通过C++头文件<cstdarg>中的va_start函数可以以字符串的形式读取所有参数到一个字符指针char*(一般使用va_list类型不直接使用char*类型)中,va_arg函数一个一个按顺序并以指定类型读取所有参数,最后通过va_end函数释放保存所有参数的字符串数组即字符指针char*(一般使用va_list类型不直接使用char*类型)。

13.4 函数默认参数和占位参数
默认参数
函数默认参数是指在定义函数时,为某些参数预先设定一个默认值。当调用函数时,如果没有为这些参数提供值,函数就会使用预设的默认值。
默认参数的基本语法
默认参数的语法格式如下:
# 默认参数sum赋值为0
int func_sum(int a, int sum = 0){
sum = sum + a;
return sum;
}
# 使用时不需要为sum提供值时,sum默认使用0,实现一个0类加到99的逻辑
int sum;
for(int i=0;i<100;i++)
sum = func_sum(sum); // 不需要为sum提供值,使用默认值0
其中:
- a是必须参数,调用时必须提供值
- sum是默认参数,如果调用时不提供值,则使用
default_value为0
默认参数的使用注意点
-
参数顺序:默认参数必须放在非默认参数之后
int func_sum(int sum=0, int a) # 语法错误 { sum = sum + a; return sum; } -
只计算一次:默认参数在函数定义时计算,而非每次调用时计算
占位参数
在函数声明和函数定义时形参只给定变量类型不给定变量名就叫占位参数,一般用于区分同名函数,并且占位参数也需要在调用时传值但函数体内不使用,并且也允许使用默认参数。
# 采用默认参数和占位参数
int func_sum(int a, int, int=0, int sum=0)
{
sum = sum + a;
return sum;
}
# 调用该函数累加0到100
int sum;
for(int i=0;i<100;i++)
sum = func_sum(sum,100);
13.5 函数重载
在同一个作用域下,函数名称相同,但函数形参列表不同(包括类型不同,个数不同,顺序不同)来进行区分同名重载函数,函数返回值类型不能区分同名重载函数。
在函数调用时会更根据实参可以传递的同名重载函数形参列表不同,判断调用相应的同名重载函数。
在有默认参数的情况下,即使实现形参列表不同的同名函数重载,在函数调用时也可能出现同时满足多个实参可以传递的同名重载函数(即使形参列表不同),这种情况要尽量避免。
示例代码:两个同名函数func_sum通过形参列表不同进行区分重载
# 同名函数func_sum
int func_sum(int a, int sum=0)
{
sum = sum + a;
return sum;
}
# 同名函数func_sum
int func_sum(int a, int b, int sum=0)
{
sum = sum + a + b;
return sum;
}
# 调用该函数累加0到100
int sum;
for(int i=0;i<100;i++)
sum = func_sum(sum,100); // 通过形参列表区分使用int func_sum(int a, int sum=0)
13.6 inline关键字声明内联函数
内联函数通过在函数前加inline关键字,建议编译器将函数代码嵌入到调用处,减少函数调用的开销,适用于小型并经常调用的函数,以防止函数的调用。
# 内联函数func_sum
inline int func_sum(int a, int b, int sum=0)
{
sum = sum + a + b;
return sum;
}
# 调用该函数累加0到100
int sum;
for(int i=0;i<100;i++)
sum = func_sum(sum,100);
上述代码等价于下面代码,可以发现开销会明显减少
# 调用该函数累加0到100
int sum;
for(int i=0;i<100;i++)
sum = i + sum;
13.7 从栈的角度认识函数调用本质
函数在内存栈区中的调用过程
编写的代码经过编译器编译成一段二进制指令,然后将其存储到硬盘中,在运行时将其加载到内存中,由CPU执行这段二进制指令。运行过程中,系统为程序分配一段栈区,用于存放临时局部变量,并通过栈顶指针和栈底指针控制栈内存的分配和回收(栈区地址由高位地址向低位地址增长),并由CPU执行这段二进制命令实现。其中局部变量的定义和子函数的调用都会触发栈内存的分配,同时局部变量的回收和子函数调用的结束都会触发栈内存的回收。在递归函数中,由于在递归终止条件前连续调用自身,可能会造成栈区内存资源的耗尽。


函数调用约定
函数调用约定指函数调用与被调用者之间的一种协议,规定以下2个方面:如果传递参数、如何恢复栈平衡。可以在函数声明或定义时,在函数名前显示指定函数调用约定方式。
①__cdecl调用约定:默认的函数调用约定方式,也是类静态成员函数的默认调用约定方式,允许支持不定量参数方式,通过push指令将不定量参数按照某种顺序压入栈中,call指令进行函数调用,在调用结束返回后通过add指令改变压入的不定量参数的栈顶位置进行恢复栈平衡。

②__stdcall调用约定:需要显式的指定函数调用约定方式,否则不启用。通过push指令将固定量参数按照某种顺序压入栈中,call指令进行函数调用,在被调用函数的ret指令返回时恢复栈平衡。由于调用者函数不进行栈平衡的恢复,这过程由被调用者函数实现,被调用者不知道压入参数数量而不支持不定量参数方式。

③__fastcall调用约定:X64架构的默认函数调用约定方式。通过push指令先将从右到左的前两个参数放入寄存器中,剩余参数放入栈中,call指令进行函数调用,在被调用函数的ret指令返回时恢复栈平衡。由于先放入寄存器中,__fastcall调用约定方式速度较快。

④___thiscall调用约定:C++中对类成员函数的默认调用约定方式。类成员函数隐式的包含this指针作为参数,先通过push指令将从右至左的参数压入栈中,最后将this指针进行保存在eax寄存器;然后采用call指令调用类成员函数,先从eax寄存器获取this指针并保存,然后通过this指针进行偏移获取类成员变量,执行函数计算逻辑;最后通过ret指令恢复栈平衡。


14.C和C++变量类型转化
14.1 隐式类型转换方式
隐式类型转换是指编译器在不需要程序员显式指定转换的情况下,自动将一种数据类型转换为另一种数据类型的过程。这种转换通常发生在表达式中包含不同数据类型的操作数时,或在函数调用时实参与形参类型不一致等情况下。
常见的隐式类型转换场景包括:
-
算术运算中的类型提升
- 当操作数中存在不同数据类型时,较小的数据类型会被自动转换为较大的数据类型
- 例如:int + double 时,int会先转换为double
- 转换顺序:bool → char → short → int → unsigned → long → float → double → long double
-
赋值时的类型转换
- 当右值类型与左值类型不一致时,会自动将右值转换为左值类型
- 例如:double d = 10; // int 10被隐式转换为double
-
函数调用时的参数转换
- 当实参与形参类型不匹配时,会尝试隐式转换
- 例如:void func(double d); func(5); // int 5被转换为double
-
条件表达式中的转换
- 在if、while等条件判断中,非布尔值会被转换为布尔值
- 例如:if(5) {...} // int 5被转换为true
-
数组退化指针
- 在大多数表达式中,数组名会自动转换为指向其首元素的指针
- 例如:int arr[10]; int* p = arr; // 数组名arr被转换为指针
注意事项:
- 隐式转换可能导致精度损失或数据溢出
- 某些转换可能产生意外的结果,如将负数赋给无符号类型
- C++11引入explicit关键字可以阻止某些隐式转换
- 现代编译器通常会给出隐式转换的警告信息
14.2 强制类型转换方式
C风格的强制类型转换是C语言中最为基础且常用的类型转换方式,其基本语法是在目标类型前使用圆括号(),然后将要转换的变量或表达式括起来。这种转换方式具有以下特点:
-
语法简单直接:
int a = 10; double b = (double)a; // 将整型a强制转换为双精度浮点型 -
适用范围广泛:
- 可用于基本数据类型之间的转换
- 可用于指针类型之间的转换
-
潜在风险:
- 不会进行任何运行时检查
- 可能导致精度损失或数据截断
- 指针类型转换可能引发严重的内存问题
float f = 3.14159;
int i = (int)f; // 小数部分被截断,i=3
C++包括两种常用的强制类型转换方式
1. 静态类型转换:std::static_cast<T>()
std::static_cast<T>()是C++中推荐使用的强制类型转换方法,用于在具有明确转换关系的类型之间进行转换。相比C风格的类型转换,它提供了更好的类型安全性检查。
主要应用场景
-
基本数据类型转换:在数值类型之间进行转换(如int到double,float到int等)
double d = 3.14159; int i = static_cast<int>(d); // i = 3 -
枚举类型转换:在枚举和整数类型之间转换
enum Color { RED, GREEN, BLUE }; int colorValue = static_cast<int>(GREEN); // colorValue = 1 -
指针类型转换:在相关类层次结构中进行指针转换(需确保转换是安全的)
class Base {}; class Derived : public Base {}; Derived* d = new Derived(); Base* b = static_cast<Base*>(d); // 向上转换,安全 -
引用类型转换:在相关父子类层次结构中进行引用转换,子类对象强制转化为父类引用接收
Derived der; Base& bref = static_cast<Base&>(der);
语法格式
std::static_cast<目标类型>(源变量);
2. 动态类型转换:std::dynamic_cast<T>()
std::dynamic_cast<T>()主要用于在继承体系中的多态类型安全转换,它依赖于运行时类型信息(RTTI),因此在编译器禁用RTTI时无法使用。
主要特点
-
运行时类型检查:
- 在执行时会检查转换是否合法
- 对于指针转换,如果转换失败会返回空指针
- 对于引用转换,如果转换失败会抛出
std::bad_cast异常
-
应用场景:
- 将父类指针或引用安全地转换为子类指针或引用(向下转换)
- 只在处理多态类型(至少有一个虚函数的类)时有效
-
示例:
class Base { public: virtual ~Base() {} // 必须至少有一个虚函数 }; class Derived : public Base {}; Base* basePtr = new Derived(); // 指针转换 Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); if (derivedPtr) { // 转换成功 } // 引用转换 try { Derived& derivedRef = dynamic_cast<Derived&>(*basePtr); } catch (const std::bad_cast& e) { // 处理转换失败 }
语法格式
std::dynamic_cast<目标类型>(源变量);
注意事项
dynamic_cast比static_cast有更大的运行时开销- 只能用于具有多态特性的类(至少包含一个虚函数)
- 在性能关键代码中应谨慎使用
15.C和C++动态内存管理
基本概念与差异
动态内存管理是在程序运行时根据需要分配和释放堆区内存的过程,与静态内存分配(编译时确定)形成对比。堆区内存的开辟和释放必须成对出现,避免内存泄漏。
- C语言仅提供
malloc、calloc、realloc和free等基本函数 - C++除了兼容C的内存管理方式外,还提供了
new和delete操作符,以及智能指针等更高级的特性
15.1 C语言动态内存管理
采用malloc分配堆区内存,采用realloc在原堆区内存基础上开辟新大小的堆区内存,采用memset重新初始化堆区内存值,采用memcpy复制堆区内存值,采用free释放堆区内存。
核心函数
- malloc():分配指定字节数的未初始化内存
void* malloc(size_t size);
-
calloc():分配指定字节数并进行初始化内存
void* calloc(size_t num, size_t size);
-
realloc():调整已分配内存块的大小
void* realloc(void* ptr, size_t size);
-
free():释放之前分配的内存
void free(void* ptr);
- memset():重置内存指定字节数
void* memset(void* dest, int val, size_t len);
-
memcpy():复制指定src指针的len个数据
void* memcpy(void* dest, const void* src, size_t len);
-
memmove():从源内存地址
src复制num个字节到目标内存地址dest
void *memmove(void *dest, const void *src, size_t num);
15.2 C++动态内存管理
new操作符
new操作符会自动调用构造函数进行初始化堆区内存,并返回开辟堆区地址。对于类类型会自动调用相应的构造函数,相比于malloc方式只会分配堆区内存而不会调用相应的构造函数初始化。
delete操作符
与new关键字相对应使用delete关键字会释放堆区内存,相比于free方式对于类类型会自动调用析构函数。然后释放堆区地址。
new创建非数组类型堆区内存,delete释放堆区内存:new 数据类型(初始化赋值),创建堆区非数组变量内存赋初值并返回该变量内存地址即该变量指针类型。
int* pint = new int(10);
delete pint;
new创建数组类型堆区内存,delete释放堆区数组内存:new 数组类型[数组长度],返回数组首地址
int* num_array = new int[10] {1, 2, 3, 4, 2, 1, 2, 3};
delete[] num_array;
更多推荐



所有评论(0)