文章目录
关键字
C关键字(33个): auto
, break
, case
, char
, const
, continue
, default
, do
, double
, else
, enum
, extern
, float
, for
, goto
, if
, int
, long
, register
, return
, short
, signed
, sizeof
, static
, struct
, switch
, typedef
, union
, unsigned
, void
, volatile
, while
C++关键字(63个): alignas
, alignof
, and
, and_eq
, asm
, auto
, bitand
, bitor
, bool
, break
, case
, catch
, char
, char8_t
, char16_t
, char32_t
, class
, compl
, concept
, const
, consteval
, constexpr
, const_cast
, continue
, co_await
, co_return
, co_yield
, decltype
, default
, delete
, do
, double
, dynamic_cast
, else
, enum
, explicit
, export
, extern
, false
, float
, for
, friend
, goto
, if
, inline
, int
, long
, mutable
, namespace
, new
, noexcept
, not
, not_eq
, nullptr
, operator
, or
, or_eq
, private
, protected
, public
, reflexpr
, register
, reinterpret_cast
, requires
, return
, short
, signed
, sizeof
, static
, static_assert
, static_cast
, struct
, switch
, synchronized
, template
, this
, thread_local
, throw
, true
, try
, typedef
, typeid
, typename
, union
, unsigned
, using
, virtual
, void
, volatile
, wchar_t
, while
, xor
, xor_eq
- 控制流关键字:
if
:用于条件语句,根据指定条件执行代码块。else
:if
语句中的可选分支,用于处理条件不满足的情况。switch
:多路分支语句,根据不同的情况执行相应的代码块。case
:switch
语句中的一个分支,用于匹配特定的值。default
:switch
语句中的默认分支,用于处理没有匹配到任何case
的情况。while
:循环语句,当指定条件满足时,重复执行循环体内的代码。do
:循环语句,先执行循环体,然后根据条件重复执行,条件满足时继续执行,直到条件不满足为止。for
:循环语句,按照指定条件重复执行,通常用于遍历序列或执行固定次数的操作。break
:用于跳出当前循环或开关语句,终止循环或分支的执行。continue
:用于跳过当前循环中的剩余语句,并开始下一次迭代。
- 变量和类型关键字:
int
:用于定义整数类型。char
:用于定义字符类型。float
:用于定义单精度浮点数类型。double
:用于定义双精度浮点数类型。long
:用于定义长整数类型。short
:用于定义短整数类型。unsigned
:用于定义无符号整数类型。signed
:用于定义带符号整数类型。bool
:用于定义布尔类型,表示真或假。void
:表示无类型或函数不返回值。auto
:用于自动类型推断。
/*
使用auto关键字声明变量时,编译器会根据变量初始化语句推断变量的类型,
从而自动确定变量的类型,避免了手动指定变量类型带来的繁琐和问题。
auto关键字仅用于声明变量时,不能用于函数的返回类型或函数参数类型的声明。
*/
auto num = 42; // 推断num为int类型
auto str = "hello"; // 推断str为const char*
auto func = [](int a) { return a * 2; }; // 推断func为lambda表达式类型
const
:用于定义常量或指定函数不会修改类成员。
const int a = 10;
typedef
:用于创建类型别名。
typedef int* IntPtr;
IntPtr p = new int;
*p = 10;
sizeof
:用于返回对象或类型的大小(字节数)。
int a = 10;
size_t size = sizeof(int); // 返回4,因为int类型占4字节
size_t size2 = sizeof(a); // 返回4,因为变量a所占的空间也是4字节
int arr[5] = {1, 2, 3, 4, 5};
size_t arr_size = sizeof(arr); // 返回20,因为5个int型元素共占20字节
size_t ele_size = sizeof(arr[0]); // 返回4,因为每个int型元素占4字节
- 函数和作用域关键字:
-
return
:用于从函数中返回值。 -
extern
:用于声明外部变量或函数。 -
static
:用于声明静态变量、函数和类成员。
static关键字用于声明静态变量、函数和类成员,具有控制作用域和生命周期的功能。,可以用于以下方面:-
声明静态变量:在函数内部,可以使用static关键字声明静态变量。静态变量存储在全局数据区,只被初始化一次,在整个程序的执行期间保持其值不变。
static int count = 1;
-
声明静态函数:使用static关键字声明的函数只能在当前文件中使用,不能被其他文件调用。声明静态函数的主要目的是隐藏函数的实现细节,在注重程序安全性的情况下防止对内部函数的滥用。
static void staticDisplay()
-
声明静态类成员:使用static关键字声明类的静态成员,可以在所有该类的实例中共享。静态成员是在程序启动时被初始化,只被初始化一次,而不是每次创建类实例时都被初始化。
-
-
register
:用于声明寄存器变量。
-
#include <stdio.h>
int main() {
register int i; // 声明寄存器变量i
int sum = 0;
for (i = 1; i <= 1000; i++) {
sum += i;
}
printf("%d\n", sum);
return 0;
}
register是C语言中的一个关键字,用于声明寄存器变量。它的作用是建议编译器将该变量存储在寄存器中,以提高变量的访问速度。然而,这只是一个建议,编译器并不一定会遵循它。在现代编译器中,优化已经足够好,很少需要使用register来优化代码。此外,C语言标准中register关键字已经被废弃,现在很少使用。
inline
:用于内联函数,指示编译器在调用处展开函数体。这可以减少函数调用的开销,提高程序执行效率。但是,使用inline修饰符并不保证函数一定会被展开,这取决于编译器的优化策略。
inline int add(int a, int b) {
return a + b;
}
int result = a + b;
namespace
:
- 其他关键字:
goto
:用于无条件跳转到程序中的标签。
goto语句是一种控制语句,它可以将程序跳转到指定的标签处执行。通常,goto语句用于在程序中实现无条件跳转,它可以使程序跳过一些语句或者循环执行某些代码。使用goto语句时,需要在程序中标记需要跳转的位置,即使用语法“标签:语句”来标记。例如:
start:
for (int i = 0; i < 10; i++) {
if (i == 5) {
goto end;
}
cout << i << " ";
}
end:
cout << "Done!";
在上面的例子中,我们使用了标签“end”来标记一个位置,然后在循环中使用goto语句跳转到了该位置。当i的值等于5时,goto语句执行,跳转到了标签“end”处,输出“Done!”。需要注意的是,使用goto语句可能会使程序的流程结构变得混乱和难以理解,所以在实际开发中应该慎用。
enum
:用于定义枚举类型。
enum是C++中的一种数据类型,用于定义枚举类型。枚举类型是一种由程序员定义的数据类型,它只能取枚举中定义的值,常用于代替复杂的数字常量,使程序更加易读、易维护。
枚举类型通常定义在函数外部或命名空间内,语法如下:
enum 枚举名{
标识符[=整型常数],
标识符[=整型常数],
…
};
其中,枚举名为枚举类型的名称,标识符为枚举量的名称,整型常数为赋值给枚举量的值,如果没有显式赋值,则默认为整数0、1、2……依次递增。
例如,定义一个月份的枚举类型:
enum Month {
JAN = 1,
FEB,
MAR,
APR,
MAY,
JUN,
JUL,
AUG,
SEP,
OCT,
NOV,
DEC
};
上面的例子中,我们定义了一个枚举类型Month,并赋予每个月份不同的整型值。注意,由于第一个月份JAN被赋值为1,因此后续的月份默认从2开始递增。可以使用枚举类型来声明变量:
Month m = MAR;
这样,m就被赋值为枚举类型中定义的三月份。
volatile
:用于声明易变变量。
#include <iostream>
using namespace std;
int main()
{
volatile int counter = 0; //声明一个volatile变量
int i;
for (i = 0; i < 10; i++)
{
counter++; //对计数器变量进行自增操作
}
cout << "Counter = " << counter << endl;
return 0;
}
在上面的示例中,我们声明了一个volatile变量counter,并在循环中对其进行自增操作。由于该变量被声明为volatile,编译器不会对其进行优化,而是每次都从内存中读取其值进行操作,从而确保了变量值的正确性。如果不使用volatile关键字,有可能会因为编译器的优化而导致计数器变量的值不正确。
- 类相关关键字:
class
:定义类。private
:指定类成员的私有访问权限。protected
:指定类成员的保护访问权限。public
:指定类成员的公有访问权限。friend
:声明友元函数或友元类。
- 继承和多态关键字:
virtual
:声明虚函数或实现动态多态性。override
:在派生类中重写基类的虚函数。final
:阻止派生类进一步重写虚函数。
#include <iostream>
using namespace std;
class MyClass
{
private:
int value;
public:
MyClass(int v) : value(v) {}
void printValue()
{
cout << "Value = " << value << endl;
}
// 声明友元函数
friend void modifyValue(MyClass& obj, int v);
};
// 定义友元函数
void modifyValue(MyClass& obj, int v)
{
obj.value = v;
}
int main()
{
MyClass obj(10);
obj.printValue();
modifyValue(obj, 20);
obj.printValue();
return 0;
}
友元函数可以访问类中的所有成员变量和成员函数,
- 异常处理关键字:
try
:将可能引发异常的代码块标记为异常尝试块。catch
:捕获并处理异常。throw
:引发异常。
throw
是一种语句,在程序运行时可以使用它来手动抛出一个异常。当程序执行到throw
语句时,会抛出一个指定的异常对象。使用throw
抛出异常后,程序会立即停止执行当前方法的代码,将异常信息传递给调用方处理。
举个例子,下面的代码演示了如何使用throw
来抛出一个自定义异常:
public class CustomException extends Exception {
public CustomException(String message) {
super(message);
}
}
public class Example {
public static void main(String[] args) {
try {
throw new CustomException("This is a custom exception message.");
} catch (CustomException e) {
System.out.println(e.getMessage());
}
}
}
在上面的代码中,我们定义了一个自定义异常 CustomException
,该异常继承了Java标准库的 Exception
类。在main
方法中使用throw
语句手动抛出了一个 CustomException
异常对象,然后使用catch
语句捕获该异常并输出异常信息。
输入输出
对象 | 用途 |
---|---|
cin | 标准输入 |
cout | 标准输出 |
cerr | 标准错误 |
clog | 输出运行时的一般性消息 |
"cerr"是C++中的一个输出流对象,用于将数据输出到标准错误流(即stderr),通常用于输出程序中的错误信息。与标准输出流不同,标准错误流独立于标准输出流,可以输出不影响程序运行的错误信息。例如:
#include <iostream>
int main() {
int x = 10;
if(x > 5) {
std::cerr << "Error: x should not exceed 5." << std::endl;
return 1; // 返回非0值表示程序出错
}
std::cout << "x is " << x << std::endl;
return 0; // 返回0表示程序正常结束
}
在上面的程序中,当变量x的值大于5时,程序将输出错误信息到标准错误流,并返回1表示程序出错。如果x的值不大于5,则输出x的值到标准输出流,并返回0表示程序正常结束。"cerr"是C++中的一个输出流对象,用于将数据输出到标准错误流(即stderr),通常用于输出程序中的错误信息。与标准输出流不同,标准错误流独立于标准输出流,可以输出不影响程序运行的错误信息。例如:
#include <iostream>
int main() {
int x = 10;
if(x > 5) {
std::cerr << "Error: x should not exceed 5." << std::endl;
return 1; // 返回非0值表示程序出错
}
std::cout << "x is " << x << std::endl;
return 0; // 返回0表示程序正常结束
}
在上面的程序中,当变量x的值大于5时,程序将输出错误信息到标准错误流,并返回1表示程序出错。如果x的值不大于5,则输出x的值到标准输出流,并返回0表示程序正常结束。
数据类型
数据类型 | 描述 | 内存大小 | 最小尺寸(位) | 代码示例 |
---|---|---|---|---|
bool | 用于表示真或假的布尔值 | 1字节 | - | bool flag = true; |
char | 用于存储字符 | 1字节 | 8 | char ch = 'A'; |
unsigned char | 无符号字符 | 1字节 | 8 | unsigned char value = 255; |
wchar_t | 宽字符类型 | 2或4字节 | 16或32 | wchar_t wc = L'文'; |
char16_t | 用于表示UTF-16编码的字符 | 2字节 | 16 | char16_t ch = u'A'; |
char32_t | 用于表示UTF-32编码的字符 | 4字节 | 32 | char32_t ch = U'文'; |
short | 短整数类型 | 2字节 | 16 | short num = 10; |
unsigned short | 无符号短整数类型 | 2字节 | 16 | unsigned short num = 65535; |
int | 整数类型 | 4字节 | 16 | int num = 42; |
unsigned int | 无符号整数类型 | 4字节 | 16 | unsigned int num = 123456; |
long | 长整数类型 | 4或8字节 | 32或64 | long num = 987654321; |
unsigned long | 无符号长整数类型 | 4或8字节 | 32或64 | unsigned long num = 4294967295; |
long long | 非常长的整数类型 | 8字节 | 64 | long long num = 1234567890LL; |
unsigned long long | 无符号非常长的整数类型 | 8字节 | 64 | unsigned long long num = 18446744073709551615ULL; |
float | 单精度浮点数类型 | 4字节 | 32 | float value = 3.14f; |
double | 双精度浮点数类型 | 8字节 | 64 | double value = 3.14; |
long double | 扩展精度浮点数类型 | 8或16字节 | 64或128 | long double value = 3.14L; |
T* | 指向类型为T的指针 | 4或8字节 | 32或64 | int* ptr = nullptr; |
指针在实质上是一个内存地址,内存地址的长度跟CPU的寻址有关。
- 在32位系统上, CPU用32位表示一个内存地址。这样的系统上一个指针占据4个字节。
- 在64位系统上, CPU用64位表示一个内存地址。这样的系统上一个指针占据8个字节。
空指针 :值为0的指针,空指针合法但是不指向任何对象。nullPtr是表示空指针的字面值常量。
void*:可以指向任意非常量的指针类型,不能执行解引用操作。
引用(Reference)
是C++中的一种数据类型,用于给一个已存在的对象起一个别名。通过引用,可以使用一个已经存在的变量来访问或修改相同的数据,而不是创建一个新的变量。引用提供了一种方便且易于理解的方式来操作对象,同时避免了不必要的复制和内存开销。
引用的语法形式是在变量名前面加上&符号来声明,如下所示:
type& reference_name = existing_variable;
引用与被引用的变量始终指向相同的内存位置,因此对引用的修改会同时影响被引用的变量。引用在声明时必须初始化,并且一旦初始化后,就不能再引用其他对象。
#include <iostream>
int main() {
int original_num = 42;
int& ref_num = original_num; // 创建引用,并将其绑定到original_num
std::cout << "original_num: " << original_num << std::endl; // 输出: 42
std::cout << "ref_num: " << ref_num << std::endl; // 输出: 42
ref_num = 99; // 修改引用的值
std::cout << "original_num: " << original_num << std::endl; // 输出: 99
std::cout << "ref_num: " << ref_num << std::endl; // 输出: 99
return 0;
}
变量、常量和宏
变量、常量和宏是在编程中常用的概念,它们有以下区别:
区别 | 变量 | 常量 | 宏 |
---|---|---|---|
可变性 | 可以在程序运行过程中改变其存储的值 | 一旦赋值后,不允许再次修改其值 | 替换为预定义的值,无法修改 |
初始化 | 可以在声明时初始化,也可以在稍后赋值 | 在声明时必须进行初始化 | 不需要初始化,直接替换为预定义的文本 |
作用域 | 可以在特定的作用域内创建和使用 | 可以在特定的作用域内创建和使用 | 无作用域,全局替换 |
宏不是数据类型,而是C++中的预处理指令。宏是一种文本替换机制,通过预处理器在编译之前对源代码进行替换。
变量声明与定义
一般情况下,变量的声明和定义是同时进行的,也就是在声明一个变量的同时为其分配内存空间。
变量声明是指在程序中引入一个变量的名称和类型,告诉编译器有一个变量存在,但并不分配内存空间。变量声明使用关键字和标识符来指定变量的类型和名称,例如:
extern int num; // 声明一个名为num的整数变量
在变量声明中,使用了关键字extern来指示编译器该变量是在其他地方定义的,而此处仅仅是引入其名称。
变量定义是指在程序中为一个变量分配内存空间,并可以进行初始化。变量定义包括了变量的声明,并为其分配内存空间,例如:
int num = 42; // 定义一个名为num的整数变量,并将其初始化为42
在变量定义中,除了声明变量的名称和类型外,还为其分配了内存空间,并可以进行初始化。
需要注意的是,变量在使用前必须先进行声明或定义。变量的声明可以在使用前进行,以便告诉编译器有一个变量存在;而变量的定义则需要在使用前进行,以便为其分配内存空间。
初始化方法
变量的初始化方法有以下几种:
- 直接赋值:在声明时使用等号将值赋给变量。
int x; // 变量声明
x = 5; // 直接赋值初始化
int y = 10; // 声明并初始化
int z = x + y; // 表达式赋值
- 复制初始化:使用赋值操作符将一个已存在的变量的值赋给另一个变量。
- 列表初始化:使用花括号将多个值括起来,用逗号分隔,赋给变量。
// 列表初始化示例
int a{5}; // 声明并列表初始化变量a
int b{1, 2}; // 列表初始化,使用了不兼容的初始化值,会导致编译错误
int c{3, 4, 5}; // 列表初始化,使用了不兼容的初始化值,会导致编译错误
定义在函数体内部的内置类型变量将不被初始化,其值未定义。
常量
定义:const用于定义一个变量,它的值不能被改变。const对象必须初始化。初始化方法只能使用直接赋值的方式,在声明时使用等号将值赋给常量。
const double PI = 3.14159; // 常量声明并初始化
宏是在预处理阶段进行简单的文本替换,因此不需要初始化。
#define MAX_VALUE 100
(1)const的引用:
const引用允许将一个常量引用绑定到非常量的对象、字面值或一般表达式上。一般情况下,引用的类型必须与所引用对象的类型一致,但const引用是一个例外。例如:
int num = 42;
const int& ref = num; // 允许将一个const引用绑定到非常量的对象
const int& ref2 = 123; // 允许将一个const引用绑定到字面值
const int& ref3 = num * 2; // 允许将一个const引用绑定到表达式的结果
在上述示例中,const引用ref
被绑定到了非常量整数num
,ref2
被绑定到了字面值123,ref3
被绑定到了表达式num * 2
的结果。
(2)指针和const:
通过从右往左阅读可以弄清楚指针和const的类型。例如:
int num = 42;
const int* p1 = # // p1是指向常量整数的指针,p1本身是可变的
int* const p2 = # // p2是指向整数的常量指针,p2本身是不可变的
const int* const p3 = # // p3是指向常量整数的常量指针,p3本身是不可变的
在上述示例中,p1
是指向常量整数的指针,即指针所指的对象是常量,但指针本身是可变的;p2
是指向整数的常量指针,即指针本身是不可变的,但指针所指的对象是可变的;p3
是指向常量整数的常量指针,即指针本身和指针所指的对象都是不可变的。
(3)顶层const:
顶层const表示指针本身是一个常量。在声明指针时,const关键字位于星号之前。例如:
int num = 42;
int* const p = # // p是一个指向整数的常量指针
const int* p2 = # // p2是一个指向常量整数的指针
const int* const p3 = # // p3是一个指向常量整数的常量指针
在上述示例中,p
是一个指向整数的常量指针,即指针本身是不可变的,指针所指的对象是可变的;p2
是一个指向常量整数的指针,即指针所指的对象是不可变的,指针本身是可变的;p3
是一个指向常量整数的常量指针,即指针本身和指针所指的对象都是不可变的。
(4)constexpr和常量表达式:
C++新标准引入了constexpr
关键字,允许将变量声明为constexpr
类型,以便编译器验证变量的值是否是一个常量表达式。constexpr
变量在编译时求值,并且其值必须在编译期间就可以确定。例如:
constexpr int size = 10; // size是一个常量表达式
constexpr int result = 2 * size; // result是一个常量表达式,可以在编译期间计算出结果
在上述示例中,size
和result
都被声明为constexpr
类型,它们在编译时求值,且其值可以在编译期间确定。
constexpr
变量的值可以在编译期间使用,这使得编译器可以进行更多的优化,并且可以在更多的上下文中使用常量表达式。
类型别名
typedef关键字是C++中旧版本的方式来定义类型别名,而using关键字是C++11引入的新方式。
typedef 原类型 别名
typedef int MyInt; // 定义类型别名 MyInt,表示 int 类型
using 别名 = 原类型
using MyInt = int; // 定义类型别名 MyInt,表示 int 类型
using namespace std; // 使用 using namespace std 引入 std 命名空间
头文件不应包含using声明
using关键字可以用于模板别名的定义,而typedef关键字则不能。
特点 | typedef | using |
---|---|---|
语法 | typedef 原类型 别名 | using 别名 = 原类型 |
C++11及以上 | 可用 | 可用 |
适用于模板 | 不适用 | 适用 |
更多选项 | 不支持 | 支持 |
自定义数据结构
自定义数据结构是指根据需要自行定义的一种数据类型,可以包含多个不同类型的数据成员
,并定义相应的操作和行为。在C++中,可以使用类
或结构体
来实现自定义数据结构。
类一般不定义在函数体内,为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样。
头文件的作用
头文件的作用 , 头文件包含了那些被定义一次的实体,包括但不限于:
- 类的声明和定义
- 成员函数的声明和定义
- 静态成员变量的声明和定义
- 类的常量的声明
- 重要的全局变量和函数的声明
预处理器指令
为了防止头文件的重复包含,通常使用预处理器指令来解决这个问题。常见的做法是使用条件编译指令,例如:
#ifndef HEADER_NAME
#define HEADER_NAME
// 头文件的内容
#endif
其中,HEADER_NAME
是一个预处理变量,一般使用大写字母命名。当头文件第一次被包含时,HEADER_NAME
未定义,条件为真,预处理器会将头文件的内容包含进来并定义HEADER_NAME
。当同一头文件再次被包含时,HEADER_NAME
已经定义,条件为假,预处理器会跳过头文件的内容,避免重复定义。
类
C++ 在 C 语言的基础上增加了面向对象编程,C++ 支持面向对象程序设计。类的基本思想是数据抽象和封装,是一种封装了数据和函数的组合。
定义抽象数据类型
(1) this指针
任何对类成员的直接访问都被视为对this指针的隐式引用。
例如,下面的代码中,成员函数isbn()
中的bookNo
实际上是对this->bookNo
的隐式引用:
cppCopy codestd::string isbn() const {
return bookNo;
}
- this指针指向当前对象的地址,可以用来访问当前对象的成员变量和成员函数。
- 在成员函数内部,可以省略this指针的显式使用,直接访问成员变量和成员函数。
(2) 在类的外部定义成员函数
如果在类的外部定义成员函数,函数名必须包含所属的类名,以表示该函数属于哪个类。
例如,下面的代码中,avg_price()成员函数在类外部进行定义:
double Sales_data::avg_price() const {
if (units_sold != 0)
return revenue / units_sold;
else
return 0;
}
(3)构造函数
定义:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。
-
构造函数没有返回类型;
-
构造函数的名字和类名相同。
-
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。
-
编译器创建的构造函数被称为合成的默认构造函数。
只有当类没有声明任何构造函数的时,编译器才会自动的生成默认构造函数。
一旦我们定义了一些其他的构造函数,除非我们再定义一个默认的构造函数,否则类将没有默认构造函数
#include <iostream>
#include <string>
class Person {
public:
// 默认构造函数
Person() {
std::cout << "Default Constructor called" << std::endl;
name = "John Doe";
age = 0;
}
// 带参数的构造函数
Person(const std::string& n, int a) {
std::cout << "Parameterized Constructor called" << std::endl;
name = n;
age = a;
}
// 成员函数
void displayInfo() {
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
}
private:
std::string name;
int age;
};
int main() {
// 使用默认构造函数创建对象
Person person1;
person1.displayInfo();
std::cout << std::endl;
// 使用带参数的构造函数创建对象
Person person2("Alice", 25);
person2.displayInfo();
return 0;
}
>>
Default Constructor called
Name: John Doe
Age: 0
Parameterized Constructor called
Name: Alice
Age: 25
访问控制与封装
(1)访问控制
在C++中,访问控制说明符用于限定类的成员的访问权限,包括public、private和protected。这些说明符决定了类的成员在哪些地方可以被访问。
-
public
:使用public说明的成员可以在整个程序内被访问。这些成员定义了类的接口,可以被类的对象和外部代码直接访问。 -
private
:使用private说明的成员只能在类的内部访问,不能被类的对象和外部代码直接访问。private成员封装了类的实现细节,提供了类的内部状态和操作的隐藏。 -
protected
:使用protected说明的成员在类的内部和派生类中可以访问。protected成员对于外部代码来说是不可见的,但派生类可以继承并访问这些成员,用于实现类之间的继承关系和数据共享。
class MyClass {
public:
int publicMember; // 公有成员
protected:
int protectedMember; // 保护成员
private:
int privateMember; // 私有成员
};
int main() {
MyClass obj;
obj.publicMember = 10; // 可以访问公有成员
// obj.protectedMember = 20; // 无法访问保护成员(在类外部)
// obj.privateMember = 30; // 无法访问私有成员(在类外部)
return 0;
}
(2)友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
- 以friend关键字标识。
- 友元不是类的成员,不受访问控制级别的约束。
- 友元的声明仅仅制定了访问的权限,而非通常意义的函数声明。必须在友元之外再专门对函数进行一次声明。
- 友元关系可以在类的定义中声明,在类的内部或外部进行定义。
class MyClass {
private:
int data;
// 声明友元函数
friend void friendFunction(const MyClass& obj);
public:
MyClass(int value) : data(value) {}
// ...
};
// 定义友元函数
void friendFunction(const MyClass& obj) {
std::cout << "Friend Function: " << obj.data << std::endl;
}
int main() {
MyClass obj(42);
friendFunction(obj);
return 0;
}
类的其他特性
(1)重载成员函数
类的成员函数可以根据参数类型和数量进行重载,从而实现不同的行为。通过重载成员函数,可以为类提供不同的操作方式。
例如,对于类Screen
,可以重载成员函数get
,以支持不同的调用方式:
class Screen {
public:
// 重载的成员函数
char get() const;
char get(int row, int col) const;
};
Screen myScreen;
char ch = myScreen.get(); // 调用无参版本
ch = myScreen.get(0, 0); // 调用带参版本
(2)类数据成员的初始化
类数据成员可以在类内部进行初始化,使用等号(=)或花括号({})的形式进行初始化。类内初始值只能使用于静态成员或非常量静态成员。
例如,对于类Window_mgr
,可以在类内初始化一个vector
类型的成员变量:
class Window_mgr {
private:
std::vector<Screen> screens{Screen(24, 80, ' ')}; // 使用花括号进行初始化
};
类内初始值可以方便地在类定义中设置默认值,简化了类对象的初始化过程。
(3)基于const的重载
类的成员函数可以根据对象是否为const
来重载。通过使用const
关键字,可以定义一个const
成员函数版本和一个非const
成员函数版本。
例如,对于类Screen
,可以重载成员函数display
:
class Screen {
public:
// 非const版本
Screen& display(std::ostream& os) {
do_display(os);
return *this;
}
// const版本
const Screen& display(std::ostream& os) const {
do_display(os);
return *this;
}
};
当某个对象调用display
函数时,根据对象是否为const
来决定调用哪个版本的函数。这样可以实现对const
对象和非const
对象的不同处理逻辑。
(4)类类型
在C++中,类在使用之前必须被定义,而不能仅仅被声明。这意味着在创建类的对象之前,该类必须已经有定义。这是因为编译器需要了解类的成员、大小和布局等信息,以便正确地创建和操作对象。
类的静态成员
在类的静态成员的声明和使用方面,可以按照以下方式进行操作:
声明静态成员:
在成员的声明之前加上关键字 static,以将其定义为类的静态成员。静态成员属于整个类,而不是类的实例。例如,声明一个静态成员函数和一个静态成员变量:
class Account {
public:
static double rate(); // 静态成员函数声明
static double interestRate; // 静态成员变量声明
};
使用类的静态成员:
可以使用作用域解析运算符 ::
来访问类的静态成员。使用类名加上作用域解析运算符,后跟静态成员的名称来调用静态成员函数或访问静态成员变量。例如:
double r;
r = Account::rate(); // 调用静态成员函数
double interest = Account::interestRate; // 访问静态成员变量
在上述示例中,rate() 是一个静态成员函数,通过 Account::rate() 来调用。interestRate 是一个静态成员变量,通过 Account::interestRate 来访问。
- 静态成员函数中不能直接访问非静态成员变量,因为静态成员函数没有隐式的 this 指针,它们无法访问特定对象的数据。静态成员函数只能访问静态成员变量或调用其他静态成员函数。
- 静态成员在类的所有对象之间共享,可以在不创建类的实例的情况下使用。它们通常用于存储和处理与类相关的全局数据或功能。
结构体
在C++中,结构体是一种用于封装不同类型的数据项的复合数据类型。结构体在C++中非常重要,因为它提供了一种方式,可以将相关的数据项组合在一起。
定义结构体
在C++中,可以通过struct
关键字定义结构体。以下是定义结构体的基本语法:
struct StructName {
type1 variable1;
type2 variable2;
// 可以添加更多的成员变量...
};
其中StructName
是你定义的结构体的名称,type1
和type2
是数据成员的类型,variable1
和variable2
是数据成员的名称。
例如,定义一个代表学生信息的结构体可以如下:
struct Student {
std::string name;
int age;
double grade;
};
创建结构体对象
定义了结构体之后,可以创建结构体的实例(对象):
Student student1;
访问结构体成员
可以使用点运算符(.
)来访问结构体的成员:
student1.name = "Alice";
student1.age = 20;
student1.grade = 3.9;
结构体初始化
可以在创建结构体对象时初始化其成员:
Student student2 = {"Bob", 22, 3.7};
结构体作为函数参数
结构体可以作为函数的参数传递,也可以作为函数的返回值。
void printStudentInfo(Student s) {
std::cout << "Name: " << s.name << std::endl;
std::cout << "Age: " << s.age << std::endl;
std::cout << "Grade: " << s.grade << std::endl;
}
Student createStudent() {
Student s;
s.name = "Charlie";
s.age = 21;
s.grade = 3.8;
return s;
}
指向结构体的指针
与其他类型的变量一样,也可以有指向结构体的指针。可以使用箭头运算符(->
)来访问结构体指针指向的成员:
Student* p = &student1;
p->name = "Alice";
结构体中的函数
在C++中,结构体不仅可以有数据成员,还可以有成员函数:
struct Student {
std::string name;
int age;
double grade;
void printInfo() {
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "Grade: " << grade << std::endl;
}
};
// 调用成员函数
Student student3;
student3.name = "David";
student3.age = 23;
student3.grade = 3.6;
student3.printInfo();
字符串
string表示可变长的字符序列
在标准库类型 string
的使用中,可以按照以下方式进行定义、初始化和操作:
注意,头文件应使用 <cstring>
,而不是 <string.h>
,在C++中使用 string
类型时应使用 std::string
,而不是 sting
。
-
定义和初始化: 可以使用以下方法对
string
进行定义和初始化:-
默认初始化:
string s1;
-
使用另一个
string
初始化:string s2(s1);
-
使用字符串字面值初始化:
string s3 = "value";
-
使用重复字符初始化:
string s4(n, 'c');
在上述示例中,
s1
、s2
、s3
、s4
是string
对象的名称,通过不同的初始化方式来创建和赋值。 -
-
string
对象的操作:string
类提供了一系列操作来处理字符串对象。以下是一些常用的操作示例:-
判空:
s.empty();
-
获取字符个数:
s.size();
-
访问字符串中的特定字符:
s[n];
-
字符串连接:
s1 + s2;
-
比较字符串:
s1 <= s2; s1 > s2; s1 >= s2;
在上述示例中,
s
是一个string
对象,可以调用相应的成员函数来执行特定的操作。 -
-
处理
string
对象中的字符: 可以使用循环来处理string
对象中的字符,例如:for (char c : s) { // 对每个字符执行操作 // c 是当前字符的值 }
在上述示例中,使用范围基于范围的
for
循环遍历string
对象s
中的每个字符,并对其执行相应的操作。
向量
vector存放的是某种给定类型对象的可变长序列。在标准库类型 vector
的使用中,可以按照以下方式进行定义、初始化和操作:
-
定义和初始化
vector
对象: 可以使用以下方法对vector
进行定义和初始化:-
默认初始化:
vector<T> v1;
-
使用另一个
vector
初始化:vector<T> v2 = v1;
-
使用重复值初始化:
vector<T> v3(n, val);
-
使用默认值初始化:
vector<T> v4(n);
-
使用列表初始化:
vector<T> v5{a, b, c...}; vector<T> v5 = {a, b, c...};
在上述示例中,
T
是vector
存储的对象的类型,v1
、v2
、v3
、v4
、v5
是vector
对象的名称,可以根据需要选择适当的初始化方式。 -
-
向
vector
对象添加元素: 可以使用push_back
函数向vector
对象中添加元素。例如:vector<T> v; v.push_back(t);
在上述示例中,
v
是一个vector
对象,使用push_back
函数向其末尾添加元素t
。 -
其他
vector
操作:vector
类提供了一系列常用的操作函数,以下是一些常见的操作示例:-
判空:
v.empty();
-
获取元素个数:
v.size();
-
通过下标访问元素:
v[n];
在上述示例中,
v
是一个vector
对象,可以调用相应的成员函数来执行特定的操作。 -
请注意,只能对已经存在的元素执行下标操作。如果要添加新的元素,需要使用 push_back
函数或其他向 vector
添加元素的方法。
vector
是一个非常有用的标准库类型,可用于存储和操作一系列相同类型的对象。您可以根据自己的需求和具体场景进一步探索和使用 vector
类的其他成员函数和功能。
数组
在 C++ 中,数组是一种用于存储一系列相同类型的元素的数据结构。它们在算法和数据操作中经常被使用。在 C++ 中,也可以使用标准库提供的 std::array
或 std::vector
类来替代传统的数组,以提供更灵活和安全的功能。以下是关于数组的一些基本概念和用法:
-
数组和指针:
- 数组使用连续的内存空间来存储元素,可以通过下标运算符
[]
来访问数组中的特定元素。 - 数组的下标通常使用
size_t
类型进行定义,以确保足够的存储空间来表示数组的索引。 - 定义数组时需要指定数组的类型,不允许使用
auto
推断数组类型。 - 数组没有引用,只能通过指针来间接访问数组元素。
- 不能对指向不相关对象的指针进行比较。两个指针只有在指向同一个数组的元素时才能进行比较。
- 数组使用连续的内存空间来存储元素,可以通过下标运算符
-
多维数组: 多维数组实际上是数组的数组,可以使用嵌套的方括号来表示多维数组。例如,二维数组可以表示为
int a[row][col]
,其中row
和col
分别表示行数和列数。 -
多维数组的初始化: 可以使用嵌套的大括号和循环来初始化多维数组的元素。例如,使用双重循环来对二维数组进行初始化:
size_t cnt = 0; for (auto &row : a) { for (auto &col : row) { col = cnt; ++cnt; } }
-
指向数组的指针: 可以使用指向数组的指针来访问数组中的元素。例如,
int (*ip)[4]
表示指向含有4个整数的数组的指针。
(1)重载成员函数
类的成员函数可以根据参数类型和数量进行重载,从而实现不同的行为。通过重载成员函数,可以为类提供不同的操作方式。
例如,对于类Screen
,可以重载成员函数get
,以支持不同的调用方式:
(2)类数据成员的初始化
类数据成员可以在类内部进行初始化,使用等号(=)或花括号({})的形式进行初始化。类内初始值只能使用于静态成员或非常量静态成员。
例如,对于类Window_mgr
,可以在类内初始化一个vector
类型的成员变量:
cppCopy codeclass Window_mgr {
private:
std::vector<Screen> screens{Screen(24, 80, ' ')}; // 使用花括号进行初始化
};
类内初始值可以方便地在类定义中设置默认值,简化了类对象的初始化过程。
(3)基于const的重载
类的成员函数可以根据对象是否为const
来重载。通过使用const
关键字,可以定义一个const
成员函数版本和一个非const
成员函数版本。
例如,对于类Screen
,可以重载成员函数display
:
cppCopy codeclass Screen {
public:
// 非const版本
Screen& display(std::ostream& os) {
do_display(os);
return *this;
}
// const版本
const Screen& display(std::ostream& os) const {
do_display(os);
return *this;
}
};
当某个对象调用display
函数时,根据对象是否为const
来决定调用哪个版本的函数。这样可以实现对const
对象和非const
对象的不同处理逻辑。
(4)类类型
在C++中,类在使用之前必须被定义,而不能仅仅被声明。这意味着在创建类的对象之前,该类必须已经有定义。
这是因为编译器需要了解类的成员、大小和布局等信息,以便正确地创建和操作对象。
迭代器
是一种用于遍历容器中元素的对象,它提供了一种通用的访问和操作容器元素的方式。迭代器可以被看作是一个指向容器元素的指针,可以用于访问容器中的元素,并执行一系列操作。
以下是关于迭代器的一些基本概念和常见运算符:
- 解引用运算符
*
: 迭代器可以通过解引用运算符*
来获取其指向的元素的引用。例如,*iter
返回迭代器iter
所指向的元素的引用。 - 成员访问运算符
->
: 当迭代器指向的元素是一个对象或结构体时,可以使用成员访问运算符->
来访问该元素的成员。例如,iter->mem
等价于(*iter).mem
。 - 前缀递增和递减运算符
++
、--
: 迭代器可以使用前缀递增和递减运算符++
、--
来向前或向后移动到容器中的下一个或上一个元素。 - 相等和不等运算符
==
、!=
: 可以使用相等和不等运算符==
、!=
来比较两个迭代器是否指向同一个位置。 - 算术运算符
+
、-
: 可以使用算术运算符+
、-
对迭代器进行移动操作。例如,iter + n
可以将迭代器iter
向前移动n
个位置。 - 复合赋值运算符
+=
、-=
: 可以使用复合赋值运算符+=
、-=
对迭代器进行相对移动操作。例如,iter += n
可以将迭代器iter
向前移动n
个位置。 - 迭代器之间的减法运算符
-
: 可以使用减法运算符-
计算两个迭代器之间的距离,即它们相差的元素个数。 - 位置比较运算符
>
,>=
,<
,<=
: 可以使用位置比较运算符对迭代器进行比较,判断它们在容器中的位置关系。
需要注意的是,在使用迭代器的循环体中,通常不允许向迭代器所属的容器添加或删除元素,因为这可能会导致迭代器失效,破坏循环的一致性。
begin
和 end
是用于迭代器范围的函数,它们用于获取迭代器的起始位置和尾后位置。
在 C++ 中,begin
函数用于获取指向容器或数组的第一个元素的迭代器,而 end
函数用于获取指向容器或数组尾后位置的迭代器。以下是它们的具体使用方法:
-
对于容器(如
string
、vector
): 对于容器对象,可以直接调用它们的成员函数begin()
和end()
来获取迭代器的起始位置和尾后位置。例如:std::string str = "Hello"; auto it1 = str.begin(); // 获取字符串 str 的起始位置迭代器 auto it2 = str.end(); // 获取字符串 str 的尾后位置迭代器 std::vector<int> vec = {1, 2, 3, 4, 5}; auto it3 = vec.begin(); // 获取向量 vec 的起始位置迭代器 auto it4 = vec.end(); // 获取向量 vec 的尾后位置迭代器
在上述示例中,
it1
和it3
是指向字符串或向量的第一个元素的迭代器,it2
和it4
是指向字符串或向量尾后位置的迭代器。 -
对于数组: 对于数组,可以使用标准库函数
std::begin()
和std::end()
来获取数组的起始位置和尾后位置的指针。例如:int arr[] = {1, 2, 3, 4, 5}; int* ptr1 = std::begin(arr); // 获取数组 arr 的起始位置指针 int* ptr2 = std::end(arr); // 获取数组 arr 的尾后位置指针
在上述示例中,
ptr1
是指向数组arr
的第一个元素的指针,ptr2
是指向数组arr
尾后位置的指针。
需要注意的是,使用 begin
和 end
函数获取迭代器范围时,通常会结合循环来遍历容器或数组的元素。例如,可以使用 for
循环和迭代器范围来遍历容器中的元素:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
// 使用迭代器 it 访问当前元素
// ...
}
在上述示例中,it
是一个迭代器,它从容器的起始位置迭代到尾后位置,可以通过 *it
来访问当前元素。
优先级 | 运算符 | 描述 |
---|---|---|
1 | :: | 范围解析 |
2 | a++ a-- | 后缀自增和自减 |
2 | type() type{} | 类型构造表达式 |
2 | a() | 函数调用 |
2 | a[] | 下标 |
2 | . -> | 成员访问 |
3 | ++a --a | 前缀自增和自减 |
3 | +a -a | 一元加和一元减 |
3 | ! ~ | 逻辑非和位非 |
3 | (type) | 类型转换 |
3 | *a | 解引用 |
3 | &a | 地址取得 |
3 | sizeof | 对象大小 |
3 | new new[] | 动态内存分配 |
3 | delete delete[] | 动态内存释放 |
4 | .* ->* | 指向类成员的指针访问 |
5 | * / % | 乘、除、模 |
6 | + - | 加和减 |
7 | << >> | 左移和右移 |
8 | < <= > >= | 小于、小于等于、大于、大于等于 |
9 | == != | 等于和不等于 |
10 | & | 位与 |
11 | ^ | 位异或 |
12 | && | 逻辑与 |
13 | ?: | 条件 |
14 | = += -= *= /= %= >>= <<= &= ^= | |
15 | , | 逗号 |
语句
- 简单语句:
;
// 复合语句
{
int x = 10;
cout << "The value of x is: " << x << endl;
}
- 条件语句if语句:
int num = 5;
if (num > 0) {
cout << "The number is positive." << endl;
} else if (num < 0) {
cout << "The number is negative." << endl;
} else {
cout << "The number is zero." << endl;
}
- 条件语句switch语句:
int choice = 2;
switch (choice) {
case 1:
cout << "You chose option 1." << endl;
break;
case 2:
cout << "You chose option 2." << endl;
break;
case 3:
cout << "You chose option 3." << endl;
break;
default:
cout << "Invalid choice." << endl;
break;
}
- 迭代语句while语句:
int count = 0;
while (count < 5) {
cout << "Count: " << count << endl;
count++;
}
- 迭代语句for语句:
for (int i = 0; i < 5; i++) {
cout << "Value of i: " << i << endl;
}
- 迭代语句范围for语句(遍历数组):
int arr[] = {1, 2, 3, 4, 5};
for (int num : arr) {
cout << "Number: " << num << endl;
}
- 迭代语句do while语句:
int n = 1;
do {
cout << "Value of n: " << n << endl;
n++;
} while (n <= 5);
- 跳转语句break语句:
for (int i = 0; i < 10; i++) {
if (i == 5) {
break; // 终止循环
}
cout << "Value of i: " << i << endl;
}
- 跳转语句continue语句:
for (int i = 0; i < 10; i++) {
if (i == 3) {
continue; // 跳过本次迭代,开始下一次迭代
}
cout << "Value of i: " << i << endl;
}
- 跳转语句goto语句(应尽量避免使用):
int num = 5;
if (num < 10) {
goto label;
}
cout << "This won't be printed." << endl;
label:
cout << "Value of num: " << num << endl;
- 跳转语句return语句:
int add(int a, int b) {
int sum = a + b;
return sum; // 返回sum的值并终止函数的执行
}
- 异常处理try和catch:
try {
int age = -1;
if (age < 0) {
throw "Invalid age."; // 抛出异常
}
cout << "Age: " << age << endl;
} catch (const char* error) {
cout << "Error: " << error << endl; // 捕获并处理异常
}
IO库
C++语言本身不直接处理输入输出,而是通过一组定义在标准库(Standard Library)中的类型来处理输入输出(IO)。这些类型提供了丰富的功能和方法,方便我们进行控制台、文件和内存字符串的输入输出操作。
下面是关于C++ IO的一些常见类型和它们的作用:
- iostream:这是处理控制台输入输出的主要类型,它定义了
cin
和cout
对象。cin
用于从控制台读取输入,cout
用于向控制台输出结果。 - fstream:这是用于处理命名文件输入输出的类型。它定义了
ifstream
(用于从文件读取输入)、ofstream
(用于向文件写入输出)和fstream
(用于读写文件)这三个类。 - stringstream:这是用于在内存中处理字符串输入输出的类型。它定义了
istringstream
(用于从字符串读取输入)和ostringstream
(用于向字符串写入输出)这两个类。stringstream
可以方便地将字符串作为输入源或输出目标。 - ifstream和istringstream:它们分别是
istream
的子类,继承了istream
中的功能,并且添加了针对文件和字符串的特定功能。ifstream
用于从文件读取输入,istringstream
用于从字符串读取输入。 - ofstream和ostringstream:它们分别是
ostream
的子类,继承了ostream
中的功能,并且添加了针对文件和字符串的特定功能。ofstream
用于向文件写入输出,ostringstream
用于向字符串写入输出。
- IO对象无拷贝或复制
- 刷新输出缓冲区:在进行输出操作时,C++使用缓冲区来暂存待输出的数据,然后在合适的时机将数据输出到目标设备(例如控制台或文件)。有两个常用的方式来刷新输出缓冲区:
flush
:调用flush
函数可以刷新缓冲区,但不会输出任何额外的字符。它只是将缓冲区中的数据立即输出到目标设备。ends
:ends
是一个特殊的操作符,它会向缓冲区插入一个空字符(‘\0’),然后刷新缓冲区。相当于连续执行了插入空字符和刷新缓冲区两个操作。#include <iostream> int main() { std::cout << "This is some text." << std::flush; // 使用 flush 刷新缓冲区 std::cout << "More text." << std::ends; // 使用 ends 插入空字符并刷新缓冲区 return 0; } ```
在这个示例中,std::cout
是用于标准输出的 ostream
对象。使用 <<
运算符,我们输出了一些文本。然后,我们使用 std::flush
和 std::ends
方法来刷新输出缓冲区。
std::flush
在第一行代码中使用,它将立即刷新缓冲区,确保前面的文本被立即输出到目标设备(例如控制台)。
std::ends
在第二行代码中使用,它插入一个空字符并刷新缓冲区。这意味着前面的文本和空字符都会被立即输出。
文件输入输出
- 以下是一个示例,演示如何使用
ifstream
从文件中读取数据:
#include <iostream>
#include <fstream>
int main() {
std::ifstream inputFile("input.txt"); // 打开名为 "input.txt" 的文件进行读取
if (inputFile.is_open()) {
int num;
while (inputFile >> num) { // 使用输入操作符从文件中读取整数
std::cout << num << " ";
}
inputFile.close(); // 关闭文件
} else {
std::cout << "Failed to open the file." << std::endl;
}
return 0;
}
- ofstream:这个类用于向一个给定文件中写入数据。它继承自
ostream
,因此可以使用ostream
中定义的输出操作符(如<<
)来将数据写入文件。
以下是一个示例,演示如何使用ofstream
向文件中写入数据:
#include <iostream>
#include <fstream>
int main() {
std::ofstream outputFile("output.txt"); // 打开名为 "output.txt" 的文件进行写入
if (outputFile.is_open()) {
outputFile << "Hello, World!" << std::endl; // 使用输出操作符向文件中写入文本
outputFile << 42 << std::endl; // 写入整数
outputFile.close(); // 关闭文件
} else {
std::cout << "Failed to open the file." << std::endl;
}
return 0;
}
- fstream:这个类可以用于读写给定的文件。它同时继承自
ifstream
和ofstream
,因此可以同时进行读取和写入操作。
以下是一个示例,演示如何使用fstream
读写文件:
#include <iostream>
#include <fstream>
int main() {
std::fstream file("data.txt", std::ios::in | std::ios::out); // 打开名为 "data.txt" 的文件进行读写
if (file.is_open()) {
// 从文件中读取数据
int num;
while (file >> num) {
std::cout << num << " ";
}
// 在文件末尾写入数据
file.seekp(0, std::ios::end); // 将写指针移动到文件末尾
file << "New data";
file.close(); // 关闭文件
} else {
std::cout << "Failed to open the file." << std::endl;
}
return 0;
}
在这个示例中,我们打开名为 “data.txt” 的文件以进行读写操作。我们首先使用输入操作符 >>
从文件中读取整数,然后使用输出操作符 <<
在文件末尾写入新的数据。注意,在进行读写操作时,我们需要设置适当的打开模式(例如 std::ios::in
、std::ios::out
、std::ios::app
等)来指定读取或写入的方式。
容器
C++容器是一种数据结构,用于存储和组织一组元素。它们提供了方便的接口和操作,容器提供了一组成员函数和操作符,可以用于管理和操作元素的存储、访问、添加、删除等操作。此外,还可以通过使用迭代器(Iterator)来遍历和访问容器中的元素,迭代器提供了一种通用的方式来访问容器中的元素。
容器可以被认为是一种抽象数据类型,隐藏了底层实现的细节,并提供了一组统一的接口和行为。C++标准库提供了多种容器类模板,每个容器类都有自己特定的功能和用途。
C++容器可以分为以下两种主要类型:
顺序容器(Sequential Containers)
顺序容器按照元素的插入顺序进行存储,并且可以按照插入的顺序访问元素。
常见的顺序容器包括:
- vector(向量)
- list(链表)
- deque(双端队列)
- array(数组)
- forward_list(前向链表)
顺序容器适用于需要保留元素插入顺序的场景。
关联容器(Associative Containers)
关联容器以一种特定的方式组织元素,以便于快速的查找和访问。
常见的关联容器包括:
- set(集合)
- map(映射)
- multimap(多重映射)
关联容器适用于需要根据键(key)进行快速查找的场景。
容器类型别名
下面是关于容器类型别名的解释:
iterator
:表示容器的迭代器类型,用于遍历和访问容器中的元素。可以使用迭代器来进行元素的读取和修改操作。const_iterator
:表示容器的常量迭代器类型,用于遍历和访问容器中的元素,但不能修改元素的值。通过常量迭代器,可以保证在迭代过程中不会对容器的元素进行修改。size_type
:无符号整数类型,足够大以容纳特定容器类型的最大可能大小。通常用于表示容器的大小,如元素的数量。difference_type
:带符号整数类型,足够大以表示两个迭代器之间的距离(迭代器之间的偏移量)。通常用于计算迭代器之间的距离或表示容器的大小差异。value_type
:表示容器中元素的类型。例如,对于容器vector<int>
,value_type
是int
。reference
:表示元素的左值引用类型,与value_type&
具有相同的含义。可以通过引用来修改容器中的元素。const_reference
:表示元素的常量左值引用类型,即const value_type&
。通过常量引用,可以读取容器中的元素,但不能修改它们的值。
类型别名是给一个类型起一个别名,使得我们可以用一个更简洁、易于理解的名称来表示该类型。它们可以帮助我们避免重复编写冗长的类型名称,并提高代码的可读性和可维护性。
举个例子,假设我们有一个使用 std::vector<int>
存储整数的容器。而每次我们要写 std::vector<int>
时,都需要输入这么长的类型名称是非常麻烦的。为了简化,我们可以使用类型别名将其替换为一个更简短的名称。
#include <iostream>
#include <vector>
int main() {
using IntVector = std::vector<int>; // 定义类型别名 IntVector
IntVector numbers; // 使用类型别名创建容器
numbers.push_back(10);
numbers.push_back(20);
numbers.push_back(30);
for (IntVector::const_iterator it = numbers.begin(); it != numbers.end(); ++it) {
std::cout << *it << " ";
}
return 0;
}
容器的构造函数
构造函数是一种特殊的成员函数,用于创建对象时初始化对象的数据成员。在C++中,构造函数具有与类名称相同的名称,并且没有返回类型。
// 构造函数 MyClass(arguments) { // 初始化代码 } }; ```
C++容器的构造函数的常见用法:
- 默认构造函数:
C c;
- 默认构造函数用于创建一个空容器,没有任何元素。
- 拷贝构造函数:
C c1(c2);
- 拷贝构造函数用于创建一个新的容器
c1
,并将另一个容器c2
的内容进行拷贝。
- 拷贝构造函数用于创建一个新的容器
- 范围构造函数:
C c(b, e);
- 范围构造函数用于创建一个容器
c
,并将迭代器b
和e
指定的范围内的元素拷贝到容器中(array
类型不支持范围构造函数)。
- 范围构造函数用于创建一个容器
- 列表初始化构造函数:
C c{a, b, c...};
- 列表初始化构造函数使用大括号
{}
,可以方便地进行列表初始化,将给定的元素列表拷贝或移动到容器c
中。
- 列表初始化构造函数使用大括号
#include <iostream>
#include <vector>
int main() {
// 默认构造函数
std::vector<int> c1;
// 拷贝构造函数
std::vector<int> c2 = {1, 2, 3};
std::vector<int> c3(c2);
// 范围构造函数
std::vector<int> c4(c2.begin(), c2.end());
// 列表初始化构造函数
std::vector<int> c5{4, 5, 6};
// 打印容器的内容
for (const auto& element : c5) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
容器操作
- 赋值与swap
操作 | 说明 |
---|---|
c1 = c2 | 将容器 c1 中的元素替换为容器 c2 中的元素 |
c1 = {a, b, c...} | 将容器 c1 中的元素替换为列表中的元素(不适用于 array ) |
a.swap(b) | 交换容器 a 和 b 的元素 |
swap(a, b) | 与 a.swap(b) 等价,用于交换容器 a 和 b 的元素 |
- 大小
操作 | 说明 |
---|---|
c.size() | 返回容器 c 中元素的数量(对于 forward_list 不适用) |
c.max_size() | 返回容器 c 中可保存的最大元素数目 |
c.empty() | 若容器 c 中没有元素存储,返回 true ,否则返回 false |
- 添加/删除元素(不适用于array)
操作 | 说明 |
---|---|
c.insert(args) | 将 args 中的元素拷贝进容器 c |
c.emplace(inits) | 使用 inits 构造容器 c 中的一个元素 |
c.erase(args) | 删除容器 c 中被 args 指定的元素 |
c.clear() | 删除容器 c 中的所有元素,返回 void |
- 关系运算符
操作 | 说明 |
---|---|
== , != | 所有容器都支持相等(不等)运算符 |
< , <= , > , >= | 关系运算符(无序关联容器不支持) |
#include <iostream>
#include <vector>
#include <set>
int main() {
// 相等和不等运算符示例
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = {1, 2, 3};
std::set<int> set1 = {1, 2, 3};
if (vec1 == vec2) {
std::cout << "vec1 is equal to vec2" << std::endl;
}
if (vec1 != set1) {
std::cout << "vec1 is not equal to set1" << std::endl;
}
// 关系运算符示例
std::vector<int> vec3 = {1, 2, 3};
std::vector<int> vec4 = {4, 5, 6};
if (vec3 < vec4) {
std::cout << "vec3 is less than vec4" << std::endl;
}
if (vec4 > vec3) {
std::cout << "vec4 is greater than vec3" << std::endl;
}
return 0;
}
- 获取迭代器
操作 | 说明 |
---|---|
c.begin() , c.end() | 返回指向容器 c 的首元素和尾元素之后位置的迭代器 |
c.cbegin() , c.cend() | 返回容器 c 的常量迭代器,指向首元素和尾元素之后位置 |
- 反向容器的额外成员(不支持forward_list)
成员 | 说明 |
---|---|
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin() , c.rend() | 返回指向容器 c 的尾元素和首元素之前位置的迭代器 |
c.crbegin() , c.crend() | 返回容器 c 的常量逆序迭代器,指向尾元素和首元素之前位置 |
顺序容器操作
- 向顺序容器添加元素
示例1:使用push_back
追加到容器尾部
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec;
vec.push_back(10);
vec.push_back(20);
vec.push_back(30);
for (const auto& num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出:
10 20 30
示例2:使用 push_front
插入到容器头部
#include <iostream>
#include <deque>
int main() {
std::deque<int> deque;
deque.push_front(30);
deque.push_front(20);
deque.push_front(10);
for (const auto& num : deque) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出:
10 20 30
示例3:在容器中的特定位置添加元素,使用 insert
#include <iostream>
#include <list>
int main() {
std::list<int> list = {10, 20, 30};
std::list<int>::iterator it = list.begin();
++it; // 将元素插入到第二个位置
list.insert(it, 15);
for (const auto& num : list) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出:
10 15 20 30
示例4:插入范围内元素,使用 insert
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3};
std::vector<int> range = {4, 5};
vec.insert(vec.begin() + 2, range.begin(), range.end());
for (const auto& num : vec) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
输出:
1 2 4 5 3
示例5:使用 emplace
操作
#include <iostream>
#include <deque>
#include <string>
int main() {
std::deque<std::string> deque;
deque.emplace_front("World!");
deque.emplace_back("Hello");
deque.insert(deque.begin(), "Hi!");
for (const auto& str : deque) {
std::cout << str << " ";
}
std::cout << std::endl;
return 0;
}
输出:
Hi! World! Hello
(2)访问元素
操作 | 说明 |
---|---|
c.back() | 返回容器 c 中尾元素的引用。若容器为空,行为未定义。 |
c.front() | 返回容器 c 中首元素的引用。若容器为空,行为未定义。 |
c[n] | 返回容器 c 中下标为 n 的元素的引用。若下标越界,行为未定义。 |
c.at(n) | 返回容器 c 中下标为 n 的元素的引用。若下标越界,抛出 std::out_of_range 异常。 |
(3)删除元素
操作 | 说明 |
---|---|
c.pop_back() | 删除容器 c 中的尾元素。若容器为空,则行为未定义。 |
c.pop_front() | 删除容器 c 中的首元素。若容器为空,则行为未定义。 |
c.erase(p) | 删除迭代器 p 所指定的元素,返回一个指向被删除元素之后元素的迭代器。若 p 指向尾元素,则行为未定义。 |
c.erase(b, e) | 删除迭代器范围 [b, e) 所指定的元素,返回一个指向最后一个被删除元素之后元素的迭代器。若 e 本身就是尾后迭代器,则行为未定义。 |
c.clear() | 删除容器 c 中的所有元素。 |
(4)特殊的forwar_list操作
操作 | 说明 |
---|---|
c.before_begin() | 返回指向首元素之前位置的迭代器,用于插入元素 |
c.cbefore_begin() | 返回常量迭代器,指向首元素之前位置,用于插入元素 |
c.insert_after(p, value) | 在迭代器 p 之后插入一个值为 value 的元素,并返回迭代器 |
c.emplace_after(p, args) | 在迭代器 p 之后使用参数 args 构造一个元素,并返回迭代器 |
c.erase_after(p) | 删除迭代器 p 之后的元素,并返回迭代器 |
c.erase_after(b, e) | 删除迭代器范围 [b, e) 之后的元素,并返回迭代器 |
(5)改变容器大小
操作 | 说明 |
---|---|
c.resize(count) | 将容器 c 的大小调整为 count 。若 count 大于当前大小,则在末尾添加默认构造的元素。若 count 小于当前大小,则删除末尾的元素。 |
c.resize(count, value) | 将容器 c 的大小调整为 count 。若 count 大于当前大小,则在末尾添加值为 value 的元素。若 count 小于当前大小,则删除末尾的元素。 |
容器管理
在C++标准库中,vector和string容器采用了一种策略来减少容器空间重新分配的次数,从而提高性能。这个策略被称为容量管理或动态内存管理。
当我们向vector或string容器添加新元素时,如果当前容器的大小超过了其已分配的内存空间,就需要进行内存重新分配。为了避免频繁的内存重新分配,这些容器通常会分配比实际需要更大的内存空间。
容器预留这些额外的空间作为备用,可以用来保存更多的新元素。这个备用空间称为容器的容量(capacity)。通过增加容器的容量,可以减少内存重新分配的次数,从而提高效率。
容器的容量管理通常采用指数增长策略,即容器每次重新分配内存时,都会分配比当前容量更大的一块内存空间。这样可以保证容器能够容纳更多的元素,而不必频繁地重新分配内存。
具体来说,当容器的大小超过了当前容量时,容器会分配一块更大的内存空间,并将原有元素拷贝到新的内存空间中。然后,容器会释放原有的内存空间,并更新容量和大小信息。这样,容器就有了更多的备用空间来容纳新元素。
通过这种动态内存管理的策略,vector和string容器可以在一定程度上提高性能,并减少内存重新分配的开销。
以下是使用Markdown语法编写的关于容器大小管理的成员函数的表格:
操作 | 说明 |
---|---|
c.shrink_to_fit() | 将容器 c 的容量减少为与大小相同的大小。 |
c.capacity() | 返回容器 c 当前的容量,即容器可以保存的元素数量的上限。 |
c.reserve(n) | 分配至少能够容纳 n 个元素的内存空间。 |
shrink_to_fit()
函数可以要求容器释放多余的备用空间,以减少内存占用。
capacity()
函数返回容器当前的容量大小,即容器可以保存的元素数量的上限。
reserve(n)
函数用于预先分配足够的内存空间,以避免频繁的内存重新分配操作。
capcacity
和size
:区别:
容器的size是指它已经保存的元素的数目;
capcacity则是在不分配新的内存空间的前提下它最多可以保存多少元素。
注意:只有当迫不得已时才可以分配新的内存空间。
string操作
(1)构造string的其他方法
构造方法 | 说明 |
---|---|
string s(cp, n) | 构造一个字符串 s ,其内容为指针 cp 指向的数组中前 n 个字符的拷贝。 |
string s(s2, pos2) | 构造一个字符串 s ,其内容为字符串 s2 从下标 pos2 开始的字符的拷贝。 |
string s(s2, pos2, len2) | 构造一个字符串 s ,其内容为字符串 s2 从下标 pos2 开始的 len2 个字符的拷贝。 |
s.substr(pos, n) | 返回一个新的字符串,其中包含字符串 s 从下标 pos 开始的 n 个字符的拷贝。 |
(2)改变string的其他方法
操作 | 说明 |
---|---|
assign() | 替换赋值,总是替换整个string中的内容。 |
insert() | 在指定位置插入字符或字符串。 |
append() | 在末尾追加字符或字符串,总是将新字符追加到string末尾。 |
replace() | 删除指定范围的字符,并在该位置插入新的字符或字符串。 |
(3)string搜索操作
操作 | 说明 |
---|---|
s.find(args) | 在字符串 s 中查找 args 第一次出现的位置,并返回该位置的索引值。 |
s.rfind(args) | 在字符串 s 中查找 args 最后一次出现的位置,并返回该位置的索引值。 |
s.find_first_of(args) | 在字符串 s 中查找 args 中任何一个字符第一次出现的位置,并返回该位置的索引值。 |
s.find_last_of(args) | 在字符串 s 中查找 args 中任何一个字符最后一次出现的位置,并返回该位置的索引值。 |
s.find_first_not_of(args) | 在字符串 s 中查找第一个不在 args 中的字符,并返回该字符的位置的索引值。 |
s.find_last_not_of(args) | 在字符串 s 中查找最后一个不在 args 中的字符,并返回该字符的位置的索引值。 |
(4)compare函数
compare() 函数 | 说明 |
---|---|
int compare(const string& str) const | 将当前字符串与参数字符串 str 进行比较,返回比较结果的整数值。 |
int compare(const char* s) const | 将当前字符串与 C 风格字符串 s 进行比较,返回比较结果的整数值。 |
int compare(size_type pos, size_type len, const string& str) const | 从当前字符串的下标 pos 处开始,与参数字符串 str 进行指定长度的比较,返回比较结果的整数值。 |
int compare(size_type pos, size_type len, const char* s) const | 从当前字符串的下标 pos 处开始,与 C 风格字符串 s 进行指定长度的比较,返回比较结果的整数值。 |
int compare(size_type pos1, size_type len1, const string& str, size_type pos2, size_type len2) const | 从当前字符串的下标 pos1 处开始,与参数字符串 str 的指定范围进行比较,返回比较结果的整数值。 |
int compare(size_type pos1, size_type len1, const char* s, size_type len2) const | 从当前字符串的下标 pos1 处开始,与 C 风格字符串 s 的指定范围进行比较,返回比较结果的整数值。 |
int compare(const string& str) const
中的最后一个 const
关键字表示该函数是一个常量成员函数。常量成员函数保证在其内部不会修改类的成员变量(除了被声明为 mutable
的成员变量)。因此,该 compare
函数在调用过程中不会修改对象的状态。
(5)数值转换
-
to_string()
:to_string()
函数用于将数值类型转换为对应的字符串表示。它接受一个数值作为参数,并返回表示该数值的字符串。例如,可以使用to_string()
将整数、浮点数等转换为字符串。int num = 10; string str = to_string(num); // 将整数转换为字符串
-
stod()
:stod()
函数用于将字符串转换为对应的双精度浮点数。它接受一个字符串作为参数,并返回转换后的浮点数。如果字符串无法转换为有效的浮点数,stod()
函数会抛出std::invalid_argument
异常。string str = "3.14"; double num = stod(str); // 将字符串转换为浮点数
容器适配器
容器适配器通常会对底层容器进行一些限制和定制,以实现特定的数据结构或算法。它们可以提供特定的插入、删除、访问等操作,并且可能对元素的顺序、优先级等进行管理。通过使用容器适配器,我们可以更轻松地在代码中使用特定的数据结构或算法,而无需直接处理底层容器的细节。
适配器是一种机制,用于将一种事物转换成另一种事物,使其具备相似的接口和行为。在软件开发中,适配器模式常用于解决不兼容接口或不同系统之间的集成问题。
C++标准库中常见的容器适配器有以下三种:
-
stack
(栈):stack
是基于底层容器实现的一种后进先出(LIFO)的数据结构。它提供了push()
、pop()
、top()
等操作,用于在栈的顶部添加元素、移除元素和访问栈顶元素。 -
queue
(队列):queue
是基于底层容器实现的一种先进先出(FIFO)的数据结构。它提供了push()
、pop()
、front()
、back()
等操作,用于在队列的尾部添加元素、移除元素和访问队列的头部和尾部元素。 -
priority_queue
(优先队列):priority_queue
是基于底层容器实现的一种优先级队列,它的元素按照一定的优先级顺序进行排列。它提供了push()
、pop()
、top()
等操作,用于添加元素、移除元素和访问优先队列中的顶部元素。
以下是关于容器适配器的表格,其中包含了它们的特点和使用场景:
容器适配器 | 特点 | 使用场景 |
---|---|---|
stack | 后进先出(LIFO)的数据结构 | - 管理函数调用的调用栈 - 实现深度优先搜索算法 - 解决需要按照后进先出顺序处理数据的问题 |
queue | 先进先出(FIFO)的数据结构 | - 处理任务队列 - 实现广度优先搜索算法 - 解决需要按照先进先出顺序处理数据的问题 |
priority_queue | 基于优先级的队列,按照元素的优先级顺序进行排列 | - 处理具有优先级的任务 - 解决需要按照元素优先级顺序处理数据的问题 |
下面是每种容器适配器的简单示例:
- 栈适配器
栈操作 | 描述 |
---|---|
s.pop() | 删除栈顶元素,但不返回该元素值 |
s.push(item) | 创建一个新元素压入栈顶,该元素通过拷贝或移动 item 而来,或者由 args 构造 |
s.emplace(args) | 由 args 构造一个新元素,并将其压入栈顶 |
s.top() | 返回栈顶元素,但不将元素弹出栈 |
s.empty() | 若栈为空,返回 true ,否则返回 false |
s.size() | 返回栈中元素的数量 |
这些操作允许您对栈适配器进行插入、删除和访问操作,以及查询栈的状态。
请注意,栈适配器是基于底层容器实现的,通常使用 deque
或 list
作为其底层容器,默认情况下使用 deque
。但您也可以通过在声明栈适配器时指定不同的底层容器类型来进行定制。
以下是栈适配器的操作的源代码示例:
#include <iostream>
#include <stack>
int main() {
std::stack<int> myStack;
// 添加元素到栈顶
myStack.push(10);
myStack.push(20);
myStack.push(30);
// 访问并弹出栈顶元素
while (!myStack.empty()) {
std::cout << myStack.top() << " ";
myStack.pop();
}
return 0;
}
在上述示例中,我们使用了 <stack>
头文件提供的栈适配器 std::stack
。通过 push()
操作,我们将元素 10、20 和 30 依次添加到栈顶。然后,通过循环遍历栈,使用 top()
获取栈顶元素的值并输出,然后使用 pop()
弹出栈顶元素。最终,栈中的元素被全部弹出,输出结果为 30 20 10
。
这个示例展示了栈适配器的基本用法,包括插入元素、访问栈顶元素和弹出元素。您可以根据需要自行扩展和修改示例代码。
- 队列适配器:
队列操作 | 描述 |
---|---|
q.pop() | 返回队列的首元素或优先队列的最高优先级元素,但不删除该元素 |
q.front() | 返回队列的首元素,但不删除该元素 |
q.back() | 返回队列的尾元素,但不删除该元素 |
q.top() | 返回优先队列的最高优先级元素,但不删除该元素 |
q.push(item) | 在队列的末尾或优先队列的适当位置创建一个元素,其值为 item |
q.emplace(args) | 在队列的末尾或优先队列的适当位置创建一个元素,通过参数 args 构造 |
q.empty() | 若队列为空,返回 true ,否则返回 false |
q.size() | 返回队列中元素的数量 |
这些操作允许您对队列适配器进行元素的插入、删除和访问操作,以及查询队列的状态。
队列适配器是一种容器适配器,它基于底层容器实现了队列的功能。队列是一种先进先出(FIFO)的数据结构,允许在队尾插入元素,并在队头删除元素。
以下是一个简单的示例,展示如何使用队列适配器:
#include <iostream>
#include <queue>
int main() {
std::queue<int> myQueue;
myQueue.push(10);
myQueue.push(20);
myQueue.push(30);
while (!myQueue.empty()) {
std::cout << myQueue.front() << " "; // 访问队头元素
myQueue.pop(); // 弹出队头元素
}
return 0;
}
输出:
10 20 30
在上述示例中,我们使用了 std::queue
容器适配器来创建一个整数类型的队列 myQueue
。通过 push()
操作,我们将元素 10、20 和 30 依次添加到队尾。然后,通过循环遍历队列并使用 front()
获取队头元素的值,最后通过 pop()
操作将队头元素弹出。
priority_queue
示例:
#include <iostream>
#include <queue>
int main() {
std::priority_queue<int> myPriorityQueue;
myPriorityQueue.push(30);
myPriorityQueue.push(10);
myPriorityQueue.push(20);
while (!myPriorityQueue.empty()) {
std::cout << myPriorityQueue.top() << " ";
myPriorityQueue.pop();
}
return 0;
}
输出:
30 20 10
关联容器
关联容器是C++标准库提供的一组容器,它们支持高效的关键字查找和访问。关联容器使用关键字来组织和索引元素,以实现快速的查找和访问操作。
以下是关联容器的表格:
类型 | 备注 |
---|---|
map | 关联数组,保存关键字-值对 |
set | 值保存关键字的容器 |
multimap | 关键字可重复出现的 map |
multiset | 关键字可重复出现的 set |
unordered_map | 用哈希函数组织的 map |
unordered_set | 用哈希函数组织的 set |
unordered_multimap | 哈希组织的 map ;关键字可以重复出现 |
unordered_multiset | 哈希组织的 set ;关键字可以重复出现 |
这些关联容器提供了不同的特性和适用场景。例如,map
和 set
是基于红黑树实现的有序关联容器,适用于需要有序访问和查找的场景。而 unordered_map
和 unordered_set
则是基于哈希表实现的无序关联容器,适用于需要快速查找和插入的场景。
(1)定义关联容器:
当定义 map
时,需要指定关键字类型和值类型。例如:
std::map<std::string, int> word_count; // 定义一个关键字为字符串、值为整数的 map
std::set<int> numbers; // 定义一个存储整数关键字的 set
注意,在定义 map
时,关键字类型和值类型需要分别指定。而在定义 set
时,只需指定关键字类型。
(2)pair 类型:
pair
是标准库类型,定义在头文件 <utility>
中。它可以存储两个数据成员,通常用于将两个值组合在一起。
当创建一个 pair
对象时,需要提供两个类型名作为模板参数。例如:
std::pair<std::string, std::string> anon; // 保存两个字符串的 pair
std::pair<std::string, std::string> author{"James", "Joyce"}; // 为每个成员提供初始化值的 pair
pair
的数据成员是公有的,并且分别命名为 first
和 second
。可以通过这两个成员来访问和操作 pair
中的数据。
以下是 pair
支持的一些操作:
操作 | 描述 |
---|---|
p.first | 访问 pair 的第一个成员 |
p.second | 访问 pair 的第二个成员 |
p = std::make_pair(a, b) | 创建一个 pair 并初始化其成员 |
std::get<0>(p) | 获取 pair 的第一个成员 |
std::get<1>(p) | 获取 pair 的第二个成员 |
p == q | 判断两个 pair 是否相等 |
p != q | 判断两个 pair 是否不相等 |
std::swap(p, q) | 交换两个 pair 的值 |
pair
提供了方便的功能,用于在需要将两个值作为单个对象处理的情况下使用。
如果您有关于关联容器或 pair
的更多问题或需要更多示例代码,请随时提问。我将尽力帮助您。
关联容器使用
这些示例代码演示了如何使用关联容器 map
和 set
:
-
使用
map
统计每个单词在输入中出现的次数:map<string, size_t> word_count; string word; while (cin >> word) ++word_count[word]; for (const auto &w : word_count) cout << w.first << " occurs " << w.second << ((w.second > 1) ? " times" : " time") << endl;
上述代码使用
map<string, size_t>
定义了一个word_count
的关联容器,用于统计每个单词在输入中出现的次数。通过循环读取输入的单词,将其作为关键字插入到map
中,并递增关键字对应的值。最后,遍历word_count
打印出每个单词及其出现次数。 -
使用
set
统计输入中每个单词出现的次数,并忽略常见单词:map<string, size_t> word_count; set<string> exclude = {"the", "But"}; string word; while (cin >> word) if (exclude.find(word) == exclude.end()) ++word_count[word];
上述代码使用
set<string>
定义了一个exclude
的关联容器,用于存储需要忽略的常见单词。在读取输入的单词时,使用exclude.find(word)
查找当前单词是否在exclude
容器中。如果不在,则将其作为关键字插入到word_count
的map
中,并递增其出现次数。
关联容器操作
下面是关于关联容器操作的表格:
操作 | 说明 |
---|---|
插入操作 | |
c.insert(v) | 将值为 v 的 value_type 对象插入容器 c |
c.emplace(args) | 使用参数 args 构造一个元素并插入容器 c |
c.insert(b, e) | 将迭代器范围 [b, e) 内的元素插入容器 c |
c.insert(il) | 将初始化列表 il 中的元素插入容器 c |
c.insert(p, v) | 在迭代器 p 指向的位置插入值为 v 的元素 |
c.emplace(p, args) | 在迭代器 p 指向的位置使用参数 args 构造一个元素 |
删除操作 | |
c.erase(k) | 删除关键字为 k 的元素,返回删除的元素数量 |
c.erase(p) | 删除迭代器 p 指向的元素,返回指向删除元素之后的迭代器 |
c.erase(b, e) | 删除迭代器范围 [b, e) 内的元素,返回迭代器 e |
下标操作 | |
c[k] | 返回关键字为 k 的元素,如果 k 不在容器中,则添加并值初始化 |
c.at(k) | 访问关键字为 k 的元素,并带有参数检查 |
元素访问 | |
c.find(k) | 返回一个迭代器,指向第一个关键字为 k 的元素 |
c.count(k) | 返回关键字等于 k 的元素的数量 |
c.lower_bound(k) | 返回一个迭代器,指向第一个关键字不小于 k 的元素 |
c.upper_bound(k) | 返回一个迭代器,指向第一个关键字大于 k 的元素 |
c.equal_range(k) | 返回一个迭代器范围,表示关键字等于 k 的元素 |
无序容器
无序容器是C++中的一种数据结构,用于存储和组织元素。与有序容器(如向量、列表、映射等)不同,无序容器并不依赖于元素的顺序,而是使用哈希函数将元素映射到桶中。无序容器提供了快速的插入、查找和删除操作。
无序容器使用关键字类型的"=="运算符来比较元素,并使用一个名为"hash<key_type>"的对象来计算元素的哈希值。哈希值用于确定元素在哪个桶中存储。
在C++标准库中,无序容器有以下几种类型:
- unordered_set:存储唯一的元素集合,不允许重复。
- unordered_multiset:存储元素集合,允许重复。
- unordered_map:存储键-值对,键是唯一的。
- unordered_multimap:存储键-值对,键可以重复。
可以使用无序容器的成员函数来管理容器中的元素,如插入、删除和查找操作。此外,还可以自定义哈希函数和比较函数,以便适应特定类型的元素。
在给定的代码片段中,使用了一个自定义的无序多重集合(SD_multiset)。它存储Sales_data类型的元素,使用名为"hasher"的哈希函数对象和名为"eqOp"的比较函数对象来组织元素。初始化时指定了桶的数量为42。可以根据实际需求修改哈希函数和比较函数,以及桶的数量。
请注意,上述代码片段中的"haser"和"eqOp"可能是变量或函数指针,用于指定自定义的哈希函数和比较函数。在此处提供的代码片段中,缺少相关定义,因此无法准确判断其含义。
以下是一个示例代码,展示了如何定义一个自定义的哈希函数和相等比较函数,并使用它们来创建一个无序多重集(unordered_multiset)对象:
#include <iostream>
#include <unordered_set>
#include <functional>
// 自定义类型
struct Sales_data {
std::string isbn;
int quantity;
double price;
};
// 自定义哈希函数
struct SalesDataHash {
std::size_t operator()(const Sales_data& sd) const {
// 这里可以根据自定义规则计算哈希值
// 这里简单地将字符串的哈希值与数量相加
return std::hash<std::string>()(sd.isbn) + std::hash<int>()(sd.quantity);
}
};
// 自定义相等比较函数
struct SalesDataEqual {
bool operator()(const Sales_data& sd1, const Sales_data& sd2) const {
// 这里可以根据自定义规则比较相等性
// 这里简单地比较ISBN和数量是否相等
return sd1.isbn == sd2.isbn && sd1.quantity == sd2.quantity;
}
};
int main() {
// 使用自定义的哈希函数和相等比较函数创建无序多重集对象
using SD_multiset = std::unordered_multiset<Sales_data, SalesDataHash, SalesDataEqual>;
SD_multiset bookStore;
// 添加一些元素到无序多重集中
Sales_data book1 = {"978-1-xxx-xxxxx-0", 2, 10.99};
Sales_data book2 = {"978-1-xxx-xxxxx-1", 3, 15.99};
Sales_data book3 = {"978-1-xxx-xxxxx-2", 2, 10.99};
bookStore.insert(book1);
bookStore.insert(book2);
bookStore.insert(book3);
// 输出无序多重集中的元素数量
std::cout << "Number of books in the store: " << bookStore.size() << std::endl;
return 0;
}
在这个示例中,我们定义了一个自定义类型Sales_data
,它包含了书籍的ISBN、数量和价格等信息。然后,我们定义了一个自定义的哈希函数SalesDataHash
和相等比较函数SalesDataEqual
,分别用于计算元素的哈希值和比较元素的相等性。
接下来,在main
函数中,我们使用自定义的哈希函数和相等比较函数创建了一个名为bookStore
的无序多重集对象。然后,我们创建了几个Sales_data
对象,并使用insert
函数将它们添加到无序多重集中。
最后,我们输出了无序多重集中的元素数量。
动态内存
动态内存是指在程序运行时通过动态分配和释放内存空间。与静态内存(由编译器在编译时分配和释放)和自动内存(由编译器在函数调用时分配和释放)不同,动态内存的分配和释放由程序员控制。
在C++中,使用关键字new
来动态分配内存,返回指向分配内存的指针。例如:
int* ptr = new int;
这将在堆(heap)上分配一个整数大小的内存块,并将指针ptr
指向该内存块。
为了释放动态分配的内存,使用关键字delete
。例如:
delete ptr;
这将释放ptr
指向的动态分配的内存块。
动态内存的使用可以带来灵活性和效率,但也需要负责地管理内存分配和释放,以避免内存泄漏或悬挂指针等问题。
在C++中,智能指针是一种用于管理动态内存的机制。它们是C++标准库提供的模板类,可以自动管理动态分配的内存,从而避免手动调用delete
。在给定的时间点上,智能指针确保只有一个指针指向动态分配的内存,并在不再需要时自动释放该内存。
C++提供了几种类型的智能指针,其中一些是:
下面是一个总结了shared_ptr
、unique_ptr
和weak_ptr
的特点的表格:
智能指针 | 特点 |
---|---|
shared_ptr | - 共享所有权的智能指针 - 多个指针可以指向相同的内存 - 引用计数管理内存的释放 - 当最后一个 shared_ptr 被销毁时释放内存 |
unique_ptr | - 独享所有权的智能指针 - 只能有一个指针指向特定的内存 - 不能直接拷贝或赋值 - 当 unique_ptr 被销毁时释放内存 |
weak_ptr | - 辅助型智能指针 - 指向由 shared_ptr 管理的对象- 不持有引用计数 - 不影响内存的生命周期 - 可以检查对象是否存在 |
(1)shared_ptr
类
shared_ptr
是 C++ 标准库中的一个智能指针类,用于共享拥有对象的所有权。每个 shared_ptr
都有一个关联的引用计数器,用于跟踪有多少个 shared_ptr
共享拥有相同的对象。
创建一个 shared_ptr
的示例:
shared_ptr<string> p1;
上述代码创建了一个空的 shared_ptr
,它没有指向任何对象。
使用 make_shared
函数可以在动态内存中分配对象并初始化它,并返回该对象的 shared_ptr
。示例:
shared_ptr<int> p3 = make_shared<int>(42);
上述代码创建了一个 shared_ptr
,它指向一个动态分配的 int
对象,并将其初始化为 42。
shared_ptr
的拷贝和赋值操作会增加对象的引用计数。当一个 shared_ptr
的引用计数变为 0 时,即没有任何指针指向该对象时,它会自动释放自己所管理的对象。这种引用计数的机制确保了在没有任何 shared_ptr
指向对象时,对象的内存会被正确释放,避免了内存泄漏。
示例:
shared_ptr<int> p2 = p3; // p2 和 p3 共享对同一个 int 对象的所有权
上述代码将增加该 int
对象的引用计数,因为现在有两个 shared_ptr
指向它。
需要注意的是,避免循环引用是使用 shared_ptr
时需要注意的问题,因为循环引用会导致对象永远无法释放。在使用 shared_ptr
时,可以使用 weak_ptr
来打破循环引用,确保内存的正确释放。
(2)unique_ptr
unique_ptr
是 C++ 标准库中的智能指针类,用于提供独享所有权的指针。与 shared_ptr
不同,unique_ptr
不能共享所有权,每个时刻只能有一个 unique_ptr
指向给定的对象。
当一个 unique_ptr
被销毁时,它所指向的对象也会被销毁,从而确保对象的正确释放。
以下是一些 unique_ptr
的常用操作:
-
创建
unique_ptr
:unique_ptr<T> u1; // 创建一个空的 unique_ptr unique_ptr<T, D> u2; // 创建一个带有自定义删除器 D 的 unique_ptr unique_ptr<T, D> u(d); // 使用自定义删除器 d 创建 unique_ptr
-
将
unique_ptr
置为nullptr
:u = nullptr; // 将 unique_ptr 置为空,不再指向任何对象
-
释放所有权:
T* ptr = u.release(); // 释放 unique_ptr 对象的所有权,并返回指针
-
重置
unique_ptr
:u.reset(); // 释放 unique_ptr 对象的所有权,将其置为空 u.reset(p); // 重置 unique_ptr,使其指向指针 p 所指向的对象 u.reset(nullptr); // 将 unique_ptr 置为空,不再指向任何对象
使用 unique_ptr
可以确保在只有一个指针指向对象时,对象的内存会被正确释放。由于 unique_ptr
不能共享所有权,因此它具有更高的性能和更轻量级的开销。它是一种适用于单一所有权的智能指针,适用于许多情况下的内存管理需求。
(3)weak_ptr
weak_ptr
是 C++ 标准库中的智能指针类,它是一种不受控制所指向对象生存期的智能指针。它指向由一个 shared_ptr
管理的对象,但不会增加引用计数,也不会改变 shared_ptr
的引用计数。
以下是一些 weak_ptr
的常用操作:
-
创建
weak_ptr
:weak_ptr<T> w; // 创建一个空的 weak_ptr weak_ptr<T> w(sp); // 创建一个指向 shared_ptr sp 管理的对象的 weak_ptr
-
将
weak_ptr
赋值给另一个weak_ptr
或shared_ptr
:w = p; // 将 `weak_ptr` 赋值为 `shared_ptr` 或另一个 `weak_ptr`
-
重置
weak_ptr
:w.reset(); // 将 `weak_ptr` 置为空,不再指向任何对象
-
获取与
weak_ptr
共享对象的shared_ptr
数量:size_t count = w.use_count(); // 获取与 `weak_ptr` 共享对象的 `shared_ptr` 的数量
-
检查
weak_ptr
所指向的对象是否存在:bool expired = w.expired(); // 检查 `weak_ptr` 所指向的对象是否已被释放
-
获取一个
shared_ptr
:shared_ptr<T> sp = w.lock(); // 获取一个 `shared_ptr`,如果对象存在则返回,否则返回空的 `shared_ptr`
在使用 weak_ptr
之前,需要通过调用 lock
方法来检查 weak_ptr
所指向的对象是否存在。如果对象存在,lock
方法将返回一个有效的 shared_ptr
;如果对象已被释放,则返回一个空的 shared_ptr
。
weak_ptr
是用于解决 shared_ptr
循环引用问题的工具。通过使用 weak_ptr
,可以避免循环引用导致的内存泄漏,同时仍然能够访问被 shared_ptr
管理的对象。
空悬指针:一个指针,指向曾经保存一个对象但现在已释放的内存。
智能指针:标准库类型。负责在恰当的时候释放内存。