写点代码相关的基本用法，以及一些数据结构等内容
PyTorch 笔记本文章归类于 代码基础 类别中，因为 PyTorch 实际上是个代码框架，并不是DL理论内容本文章中的相关代码已经公开在 GitHub 上，详情参考链接： GitHub 仓库1 Dataset获取数据集和对应的 label有以下主要功能：如何获取每一个数据和对应的 label如何获取到数据集的总量大小常用数据集格式是：一个文件夹放置所有的图像，另外将不同的图像的 label 写在对应的 txt 文件中1.1 Override1.1.1 __init__()一般放全局变量，包括路径等内容，这里使用 hymenoptera_data 数据集作为例子hymenoptera_data/ └── train/ ├── ants/ │ ├── xxx1.jpg │ ├── xxx2.png │ └── ... └── bees/ ├── xxx1.jpg ├── xxx2.png └── ...from torch.utils.data import Dataset def __init__(self, root: str, label: str): """数据的初始化工作 Args: root (str): 数据的根路径 label (str): 某一个子文件夹的名称，也就是label """ # 这里是根据不同的数据集来更改的 # 以 hymenoptera_data 数据作为例子， label 下存放了所有的数据图片 self.root_dir = root # root 路径 self.label_dir = label # 标签信息 self.img_path = os.path.join(self.root_dir, self.label_dir) # root 路径地址拼接 self.img_list = os.listdir(self.img_path)1.1.2 __getitem__()获取某一个数据的具体信息def __getitem__(self, index: int): """获取某一个数据的具体信息 Args: index (int): 数据索引号 Returns: img (ndarray, RGB), label (str) """ img_path = os.path.join(self.img_path, self.img_list[index]) img = cv2.imread(img_path) # OpenCV 默认 BGR # 读取失败检查 if img is None: raise FileNotFoundError(f"Failed to read image: {img_path}") # 转成 RGB img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) label = self.label_dir1.1.3 使用实例if __name__ == '__main__': data_root = '/Users/rainer/MyFiles/Code/Learning/Pytorch/dataset/hymenoptera_data/train' ant_data = Hymenoptera(root=data_root, label='ants') bee_data = Hymenoptera(root=data_root, label='bees') train_dataset = ant_data + bee_data print(len(ant_data), len(bee_data)) print(len(train_dataset)) test_img, test_label = ant_data[3] # 简写，等同于 __getitem__ # OpenCV 显示需要 BGR，所以转回来 img_bgr = cv2.cvtColor(test_img, cv2.COLOR_RGB2BGR) cv2.imshow(test_label, img_bgr) cv2.waitKey(0) cv2.destroyAllWindows()2 Dataloader为网络的输入提供不同的数据形式Dataloader 一般不需要自己重写，只需要了解每一个参数的作用即可from torch.utils.data import DataLoader test_loader = DataLoader( test_data, batch_size=4, # 每次读取数据数量 shuffle=True, # 是否每次打乱顺序 num_workers=0, # 子进程个数 drop_last=False # 丢弃多余数据 )3 TensorboardTensorborad 让我们可以方便的查看结果3.1 常用 APIwriter.add_image() 用于添加图像，需要指定图片的类型 (cv/numpy)writer.add_scalar() 添加标量信息，常用的 loss 可以用这个数据写主要有两个轴，纵坐标是参数 scalar_value 横坐标 global_step3.2 代码示例from torch.utils.tensorboard import SummaryWriter import cv2 as cv if __name__ == '__main__': writer = SummaryWriter(log_dir='logs') # 将事件存储到目标logs文件夹 img_path = 'dataset/hymenoptera_data/train/ants/0013035.jpg' img_cv = cv.imread(img_path) print(img_cv.shape) # HWC # 1. 添加单个图片演示 writer.add_image('test', img_cv, 0, dataformats='HWC') # 2. 添加某个标量测试 writer.add_scalar(tag='loss', scalar_value=0.5) # 3. 添加一连串的数据，类似于绘制函数图像 for i in range(100): writer.add_scalar(tag='loss2', scalar_value=2 * i, global_step=i) writer.close()3.3 查看结果注意如果 tb 的输出文件都在同一个文件下方，则会重叠因此实际使用的时候一般区分不同的子文件夹的 tensorboard --logdir=logs --port xxx并在对应的端口下方查看结果4 TransformTransform 下方是内部放置了很多的工具，不用再造轮子的使用方法from torchvision import transforms4.1 常用 APITensor 格式转化# 1.将 PIL 文件转为 Tensor格式 img_path = '/root/TestPytorch/dataset/test_data/train/ants/0013035.jpg' img = Image.open(img_path) tensor_trans = transforms.ToTensor() tensor_img = tensor_trans(img) #转化格式为tensor # print(tensor_img) # 2.使用 opencv img_cv = cv.imread(img_path) tensor_cv = tensor_trans(img_cv) # print(tensor_cv) writer = SummaryWriter('/root/TestPytorch/logs') writer.add_image('img_cv', tensor_cv, global_step=10)Normalize# 3.使用 Normalize 归一化库 # Normalize 参数包括 mean 代表均值，RGB通道则为3个值的数组，std代表标准差，公式如下 # output[channel] = (input[channel] - mean[channel]) / std[channel] print(tensor_cv[0][0][0]) normalize = transforms.Normalize(mean=[0.5, 0.5, 0.5],std = [0.5, 0.5, 0.5]) img_normalize = normalize(tensor_cv) writer.add_image('img_normalize', img_normalize, global_step=10) print(img_normalize[0][0][0])Resize# 4. 使用 Resize # 默认使用 (H, W) 参数进行输入，如果输入单一值，将会按照最小边匹配进行缩放 resize = transforms.Resize((512, 512)) print(tensor_cv.size()) img_resize = resize(tensor_cv) print(img_resize.size()) writer.add_image('img_resize', img_resize, global_step=10)Compose# 5.使用 Compose 整合操作流程 trans_compose = transforms.Compose([ transforms.Resize((512, 512)),transforms.ToTensor(), ]) img_compose = trans_compose(img) # 先进行resize 再进行totensor writer.add_image('img_compose', img_compose, global_step=10)5 Network这里是构建神经网路的组建，包含了常见的一些 API实际用的类是 nn.module5.1 nn.Module最基本的模块，网络均继承自这个类，使用方法例如forward 会对输入依次做出内部的操作import torch import torch.nn as nn class MyModel(nn.Module): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) def forward(self, x): output = x + 1 return output if __name__ == '__main__': model = MyModel() x = torch.tensor(1.0) output = model(x) print(output)5.2 Conv卷积操作，常见参数，需要注意输入 tensor 的格式要求torch.nn.functional.conv2d 要求输入张量 input 的形状严格为 (N, C_in, H, W)N：batch sizeC_in：输入通道数H, W：图像高、宽如果操作 reshape 的话，不可以全部限制死，通过将 N 指定为 -1 可以让函数自行计算 N 的大小output = torch.reshape(output,(-1, 3, 30, 30))# input: 输入tensor参数，(minibatch, in_channel, H, W) # weight: 卷积核，被卷积的一个矩阵，会反复的移动 (需要有channel参数) # bias: 偏置值 # stride: 卷积移动的步长（默认为1），也可以是一个元组(a,b)分别控制横向和纵向步长 # padding: 填充数值为0的行列，可以用元组指定，也可以是string的"same"表示控制输出同一个大小，只能在stride = 1时使用 import torch output = torch.nn.functional.conv2d(input, kernel,stride=(1,1))一个卷积层可以这样写（完整例子，下方省略类似例子）class Model(torch.nn.Module): def __init__(self): # 注意这里初始化的调用方法 super(Model, self).__init__() self.conv1 = torch.nn.Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0) def forward(self, x): return self.conv1(x)5.3 Pool池化层，也就是取某一个值来代表某组数据，这里以最大池化层作为例子，简略给出例子# torch.nn.MaxPool2d() # input 注意应该是四维array # kernel_size : 池化核大小 # stride : 窗口的步长,默认是池化核的大小 # padding : 周边填充 # dilation : 卷积中插入的数值，一般不用 # ceil_mode(默认False) : ceil模式的开关，向上取整，如果True则保留外围一圈;floor是向下取整self.pool1 = torch.nn.MaxPool2d(kernel_size=2)5.4 Activation激活函数，这里以 ReLU 作为例子，简略例子self.nonlinear = torch.nn.ReLU(inplace=False) # 创建个新数组，并非原地处理5.5 Linear线性层torch.nn.Linear(in_features, out_features, bias=True) # in_feature: 代表输入的数量 # out_feature: 代表输出的数量 # bias: 表示是否启用 偏置self.linear1 = torch.nn.Linear(in_features=196608, out_features=10)5.6 Sequential把一组操作整合在一起def __init__(self): super(Model, self).__init__() self.model1 = nn.Sequential( nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding="same"), nn.MaxPool2d(kernel_size=2), nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding="same"), nn.MaxPool2d(kernel_size=2), nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding="same"), nn.MaxPool2d(kernel_size=2), nn.Flatten(), nn.Linear(in_features=1024, out_features=64), nn.Linear(in_features=64, out_features=10) ) def forward(self, x): x = self.model1(x) return x5.7 Loss常用的有两种 ，MSE 和 Cross Entropinputs = torch.tensor([0, 1, 2],dtype=torch.float32) targets = torch.tensor([0, 1, 5],dtype=torch.float32) loss = torch.nn.MSELoss(reduction='mean') result = loss(inputs, targets) ... entropy_loss = torch.nn.CrossEntropyLoss() entropy_result = entropy_loss(x_output, targets)5.8 Backward反向传播，根据 loss 和 gradient 进行参数的更新model = Model() loss = nn.CrossEntropyLoss() for data in dataloader: inputs, labels = data output = model(inputs) result_loss = loss(output, labels) result_loss.backward() # 反向传播，注意是对结果值进行反向传播，是优化器的使用基础 break5.9 Optimizer每一个优化器都有自己的用法和参数，通用的有 lr注意优化的时候需要将梯度归零，否则前几轮会遗留影响model = Model() loss = nn.CrossEntropyLoss() # 损失函数 optimizer = torch.optim.SGD(model.parameters(), lr=0.001) # 设置优化器 for epoch in range(20): # 假设进行20轮训练，则可以观察每一轮后的loss值为多少 running_loss = 0.0 for data in dataloader: inputs, labels = data output = model(inputs) result_loss = loss(output, labels) optimizer.zero_grad() # 首先清空梯度，避免之前的循环造成影响 result_loss.backward() # 反向传播，注意是对结果值进行反向传播，是优化器的使用基础，改变了梯度值 optimizer.step() # 利用梯度进行反向传播 running_loss += result_loss.item() # 计算这一轮中所有loss的总和 print(epoch, running_loss) 6 Model模型主要分为两个部分，模型的保存与读取# 保存模型 vgg16 =torchvision.models.vgg16() torch.save(vgg16, "./model/vgg16.pth") # 读取文件 model = torch.load("./model/vgg16.pth", weights_only=False) print(model)或者可以只保存字典形式，这是更加推荐的方法torch.save(vgg16.state_dict(), "./model/vgg16_dict.pth") model = torch.load("./model/vgg16_dict.pth") print(model) # 这是一个字典 model = torchvision.models.vgg16() # 先实例化网络结构 state_dict = torch.load("./model/vgg16_dict.pth") # 加载权重字典 model.load_state_dict(state_dict) # 把权重载入模型 model.eval() # 推理模式7 训练模型以 CIFAR 10 作为例子pytorch 基本框架已经完毕，下文是实际训练相关的操作7.1 准备数据集直接使用官方的已有的方式加载，如果是自己的数据集可能需要额外的客制化import torch from torchvision import transforms train_data = torchvision.datasets.CIFAR10(root='../dataset', train=True, transform=transforms.ToTensor(), download=True) test_data = torchvision.datasets.CIFAR10(root='../dataset', train=False, transform=transforms.ToTensor(), download=True)7.2 加载数据使用 Dataloader 加载，DataLoader 一般来说不用客制化from torch.utils.data import DataLoader # 加载数据集 train_data_loader = DataLoader(train_data, batch_size=64) test_data_loader = DataLoader(test_data, batch_size=64)7.3 构建模型# 模型参见上文，此处省略 from .model import NetworkDemo model = NetworkDemo()7.4 Loss 和 优化器loss = nn.CrossEntropyLoss() learning_rate = 0.01 optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)7.5 训练模型model.train() 对于一些特殊的网络层有作用，例如 Normalize 或者 Drop outfor i in range(epoch): print(f"------ Train Epoch [{i + 1:2}] ------") # 训练步骤 model.train() for data in train_data_loader: imgs, target = data outputs = model(imgs) loss = loss_func(outputs, target) # 优化器 optimizer.zero_grad() # 务必清空梯度 loss.backward() optimizer.step() train_step += 1 if train_step % 100 == 0: print(f"Iteration [{train_step}], loss {loss.item()}") writer.add_scalar("train_loss", loss.item(), train_step)7.6 验证模型注意要关掉梯度功能， with torch.no_grad():# 测试功能 model.eval() test_loss = 0 with torch.no_grad(): # 关闭梯度功能 for data in test_data_loader: imgs, targets = data outputs = model(imgs) loss = loss_func(outputs, targets) test_loss += loss test_loss = test_loss / len(test_data_loader)7.7 使用 TensorBoard 记录数据from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter("../logs") ... if train_step % 100 == 0: print(f"Iteration [{train_step}], loss {loss.item()}") writer.add_scalar("train_loss", loss.item(), train_step) ... writer.add_scalar("test_loss_epoch", test_loss.item(), epoch)7.8 保存每一轮的训练模型torch.save(model, f"../train/model/model_{epoch}.pth") # 或者可以只保存参数 torch.save(model.state_dict(), f"../train/model/model_{epoch}.pth")7.9 GPU 训练GPU 训练的本质就是把数据和模型都放在 GPU 上即可# 选择训练设备 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = NetworkDemo() model.to(device) # 模型放到对应设备上 for i in range(epoch): print(f"------ Train Epoch [{i + 1:2}] ------") model.train() for data in train_data_loader: imgs, target = data imgs = imgs.to(device) # 数据放到 GPU target = target.to(device) # 数据放到 GPU outputs = model(imgs) loss = loss_func(outputs, target)8 模型验证8.1 读取数据cv2 的读取相对麻烦一些，可以在实际项目中自行使用（cv2 速度更快）这里用 PIL 因为更加方便import cv2 import torchvision from PIL import Image image_path = "../dataset/test/plane.jpeg" img = cv2.imread(image_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 使用 PIL 读取 img = Image.fromarray(img) transform = torchvision.transforms.Compose([ torchvision.transforms.Resize((64, 64)), torchvision.transforms.ToTensor(), ]) img = transform(img) print(img.shape)8.2 加载模型# 这里是保存时候的代码 torch.save(model.state_dict(), f"../train/model/model_{i}.pth") # 加载使用字典 model = NetworkDemo() state_dict = torch.load("../train/model/model_8.pth") model.load_state_dict(state_dict) model.eval()8.3 进行推理这里有个坑就是，把输入需要 reshape 到和网络模型完全一致# 构建 batch size img = torch.reshape(img, (1, 3, 32, 32)) with torch.no_grad(): output = model(img) print(output) print(output.argmax(1)) # 打印最大值8.4 查看结果这里用 CIFAR 作为例子，可以在 dataset 中查看每一个 label 的实际代表# 可以打印数据集中的那个类别 from torchvision.datasets import CIFAR10 dataset = CIFAR10(root="../dataset", train=False, download=False) print(dataset.classes[output.argmax(1)])这里我是用了一张飞机的图片
OOP课程笔记：第九章 标准库容器和算法
第九章 标准库容器和算法C++ 标准模板库（ STL ）的核心是 容器、算法和迭代器用来实现常用算法和数据结构容器：将多个对象存储在单个容器对象 中，并对它们进行操作迭代器：对应用在容器或其他序列上的 指针的抽象算法：也称为标准算法或泛型算法，通过迭代器作用于容器，是实现常用算法的 函数模板1.容器概览容器类分类容器类描述头文件顺序容器vector可变大小数组<vector> deque双端队列<deque> list双向链表 array固定大小数组<array> forward_list单向链表<forward_list>关联容器map键/值对，每个键只能关联一个值 multimap键/值对，每个键可以关联多个值 set集合，每个元素都是唯一的<set> multiset集合，元素可以不唯一<set>无序关联容器unordered_map用哈希函数组织的 map<unordered_map> unordered_multimap用哈希函数组织的 multimap<unordered_map> unordered_set用哈希函数组织的 set<unordered_set> unordered_multiset用哈希函数组织的 multiset<unordered_set>容器适配器stack栈<stack> queue队列<queue> priority_queue优先级队列<queue>顺序容器： 依次存放和使用容器中的每个元素关联容器：允许 基于键快速检索容器中元素的值迭代器：指向容器和其他序列的指针的抽象2.顺序容器不知道用什么就用<vector>所有顺序容器都提供了 快速顺序 访问元素的能力在 添加 和 删除元素 以及 非顺序随机访问元素 方面有不同的性能考虑顺序容器存储结构访问元素插入/删除元素独特性vector可变大小数组快速随机访问尾部快速连续存储array固定大小数组快速随机访问不支持固定大小deque双端队列快速随机访问头尾位置快速 list双向链表双向顺序任意位置快速有额外空间开销forward_list单向链表单向顺序任意位置快速不支持 size 操作如果要求 随机访问 元素，使用 vector 或 deque 如果要求在容器中间插入 或 删除 元素，使用 list 或 forward_list如果需要在 头尾位置插入 或 删除 元素，但不会在中间位置插入或删除，使用 deque定义和初始化容器类都定义了 默认构造函数 和 拷贝构造函数标准库 array 类型 不支持普通的容器构造函数array 的大小也是类型的一部分，当 定义一个 array 时，要 指定元素类型和容器大小//适用于所有容器的定义方式 vector<string> svec; //创建一个存放 string 对象的空容器 vector<string> svec1(svec); //创建 svec 的副本 //下面两种方式仅适用于顺序容器 vector<string> svec2(10,”hi”); //创建大小为 10 的容器，用 " 初始化每个元素 vector<string> svec3(10); //创建大小为 10 的容器，每个元素是空串 //定义 array 时要指定类型和大小 array<int, 3> a1; //3个 int 元素，默认初始化：全局 0 ，局部自动随机值 array<int, 4> a2 = {1,2}; //1 2 0 0 array<string, 3> a3 = {"a","bb","ccc"}; //a bb ccc赋值与交换赋值运算符 将左操作数容器中的全部元素替换为右操作数容器中元素的副本赋值运算符要求 左右操作数的类型相同 assign 操作 用参数所指定的元素的副本替换左边容器的所有元素assign 允许 从一个不同但相容的类型赋值，或者从容器的 一个子序列赋值。仅顺序容器支持 assign 操作 。 array 允许赋值，但不支持assign ，也不允许用花括号包围的值列表进行赋值。//赋值相关的运算会导致指向左边容器内部的迭代器、引用和指针失效 //赋值运算符 c1 = c2; //c1 的内容替换为 c2 中元素的副本 c1 = {1, 2, 3}; //赋值后 c1 大小为 3 //seq.assign(b, e); list<string> names; vector<const char*> cstyle; names = cstyle; //错误：容器类型不匹配 names.assign(cstyle.begin(), cstyle.end()); //正确：元素之间可以类型转换 //seq.assign(n,t) list<string> slist1(1); //1 个元素，为空 string slist1.assign(10, "hi");//10个元素都是 "hi", 替换 slist1 原有元素swap 操作交换两个 相同类型容器 的内容，是交换两个容器的内部数据结构，元素本身并未交换（容器类型为 array 和 string 的情况除外）除 array 外， swap() 不对任何元素进行复制、删除或插入操作，因此可以保证在常数时间内完成vector<string> vs1 (10) vector<string> vs2 (10) swap(vs1 , vs2);大小与比较vector<int> ivec (10,2） ivec.size(); //返回容器中当前的元素个数 ivec.empty; //判断容器是否为空，如果是，返回 true ivec.resize(15); //将容器大小改为 15 ，新添加的元素初始化为 0 ivec.resize(20,5); //将容器大小改为 20 ，新添加的元素初始化为 5 ivec.resize(5); //将容器大小改为 5 ，在尾部将多余元素删除只有其 元素类型也定义了相应的比较运算符 时，才可以使用关系运算符来比较两个容器顺序容器都定义了关系运算符，比较原则为：如果两个容器的大小相等且所有元素都对应相等，则容器相等如果不相等 ，比较的结果取决于 第一个不相等元素的比较结果访问元素顺序容器：at 和下标操作（[]）仅适用于：string、vector、array 和 dequeback() 不适用于：forward_list访问方式功能描述特殊情况处理适用容器c.back()返回容器 c 中最后一个元素的引用如果 c 为空，行为未定义除 forward_list 以外的容器c.front()返回容器 c 中第一个元素的引用如果 c 为空，行为未定义所有顺序容器c[n]返回容器 c 中下标为 n 的元素的引用（n 是无符号整数）如果 n >= c.size()，行为未定义string、vector、array、dequec.at(n)返回容器 c 中下标为 n 的元素的引用如果下标越界，抛出 out_of_range 异常string、vector、array、deque添加元素操作描述c.push_back(t)在 c 的尾部创建一个值为 t 或由 args 创建的元素。返回 voidc.emplace_back(args)在 c 的尾部创建一个值为 t 或由 args 创建的元素。返回 voidc.push_front(t)在 c 的头部创建一个值为 t 或由 args 创建的元素。返回 voidc.emplace_front(args)在 c 的头部创建一个值为 t 或由 args 创建的元素。返回 voidc.insert(p,t)在迭代器 p 指向的元素之前创建一个值为 t 或由 args 创建的元素。返回指向新添加的元素的迭代器c.emplace(p, args)在迭代器 p 指向的元素之前创建一个值为 t 或由 args 创建的元素。返回指向新添加的元素的迭代器c.insert(p,n,t)在迭代器 p 指向的元素之前插入 n 个值为 t 的元素。返回指向新添加的第一个元素的迭代器；若 n 为 0，返回 pc.insert(p,b,e)将迭代器 b 和 e 指定的范围内的元素插入到迭代器 p 指向的元素之前。b 和 e 不能指向 c 中的元素。返回指向新添加的第一个元素的迭代器；若范围为空，则返回 pc.insert(p, il)il 是一个花括号括起来的元素列表。将这些给定值插入到迭代器 p 指向的元素之前。返回指向新添加的第一个元素的迭代器；若列表为空，则返回 p删除元素vector<int> ivec (10,2); ivec.clear(); //删除容器内所有元素，函数返回 void ivec.pop_back();//删除容器内最后一个元素，函数返回void，空容器未定义该函数 ivec.pop_front();//错误， vector 不支持 pop_front 操作 ivec.erase(ivec.begin()+1); //删除容器内第二个元素 ivec.erase(ivec.begin(), ivec.begin()+2));//删除容器内前两个元素 // 迭代器是左闭右开区间3.迭代器标准库提供了迭代器访问容器内的元素，迭代器是一种可以检查并遍历容器内元素的数据类型用这个指示器可以访问当前位置的元素，迭代器对所有容器都适用begin 和 end 标记了容器中元素的一个范围，称为左闭合区间，表示范围自 begin 开始，于 end 之前结束左闭合区间的 3 种性质①如果 begin 和 end 相等，则范围为空②如果 begin 与 end 不等，则范围至少包含一个元素，且 begin指向该范围中的第一个元素③可以对 begin 递增若干次，使得 begin == end 这些性质意味着可以用类似下面的代码来循环处理一个元素范围，而且是安全的while (begin != end){ *begin = val; // 正确：范围非空 ++begin; // 移动迭代器至下一个元素 }每种容器都可以定义自己的迭代器变量容器提供的成员函数 begin() 返回指向容器第一个元素的迭代器；成员函数 end() 返回容器最后一个元素的下一个位置vector<int> iterator viter; list<string>::iterator liter; //可以使用 begin() 对迭代器变量进行初始化 vector<int> ivec (10, 2); vector<int> iterator iter1 = ivec.begin(); auto iter2 = ivec.begin相关的运算及操作运算描述*iter返回 iter 指向的元素的引用iter->mem获取 iter 指向的元素的成员 mem++iter / iter++指向容器中的下一个元素--iter / iter--指向容器中的前一个元素（forward_list 迭代器不支持）iter1 == iter2比较两个迭代器是否相等，即它们是否指向容器中的同一个元素iter1 != iter2 以下仅限于 vector 和 deque运算描述iter + n / iter - n将产生指向容器后面（前面）第 n 个元素的迭代器iter1 += iter2两个迭代器进行相加运算iter1 -= iter2两个迭代器进行相减运算iter1 - iter2注意：两个迭代器必须指向同一个容器>, >=, <, <=比较两个迭代器的位置，位置在前面的迭代器要小于位置在后面的迭代器迭代器相关的操作通过迭代器操作，可以定位和遍历容器内的元素，并进行各种操作容器上的插入和删除操作都有使用迭代器范围的版本通过容器提供的 insert 成员函数 ，可以在容器的 指定位置插入元素所有容器都提供 erase 成员函数 ，可以 删除指定位置的元素#include <iostream> #include using namespace std; int main() { list<int> ilist; ilist.insert(ilist.begin(), 5); ilist.insert(ilist.begin(), 4); ilist.insert(ilist.begin(), 3); ilist.insert(ilist.begin(), 2); ilist.insert(ilist.begin(), 1); list<int>::iterator iter = ilist.begin(), b, e; b = ++iter; e = ++iter; e = ++iter; ilist.erase(b, e); for (iter = ilist.begin(); iter != ilist.end(); iter++) cout << *iter << "\t"; }反向迭代器反向迭代器在容器中 从尾元素向首元素移动，除了 forward_list 之外的容器都有反向迭代器通过调用容器的 rbegin() 、 rend() 、 crbegin() 和 crend() 成员函数，获得反向迭代器的非 const 和 const 版本对于 反向迭代器，递增以及递减操作的含义会颠倒，对反向迭代器 rit ，++rit 会移动到前面一个元素， rit 会移动到下一个元素#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { // 使用反向迭代器逆向处理容器中的元素 // 逆序打印vec中的元素： vector<int> vec = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // 从尾元素到首元素的迭代 for (auto r_iter = vec.crbegin(); r_iter != vec.crend(); ++r_iter) { cout << *r_iter << " "; } cout << endl; // 输出: 9 8 7 6 5 4 3 2 1 0 // 反向迭代器让某些标准算法的使用更灵活。 // 算法sort(b, e)对迭代器b和e之间的元素按升序排序 // 使用反向迭代器，可以得到降序排序的序列： sort(vec.begin(), vec.end()); // 按升序对vec排序 sort(vec.rbegin(), vec.rend()); // 按降序排序 // 验证排序结果 for (auto num : vec) { cout << num << " "; } cout << endl; return 0; }4.关联容器关联容器支持键（ key ）的使用，通过 键访问元素，标准库提供 8 个关联容器类型（按键值）有序容器描述map关联数组；保存键-值对（key, value）set键即值，即只保存键的容器multimap键可重复出现的 mapmultiset键可重复出现的 set无序容器描述unordered_map用哈希函数组织的 mapunordered_set用哈希函数组织的 setunordered_multimap哈希组织的 map；键可以重复出现unordered_multiset哈希组织的 set；键可以重复出现map类型map 容器也被称为字典或关联数组，map 中的元素是 key, value 数据对其中 value 用来存储数据 key 用来做 value 的索引定义 map 时，必须指明键类型和值类型初始化 map 时，必须提供键类型和值类型， 将每个 key value 对用花括号括起： {key, value}，一起构成 map 中的一个元素下例中展示了如何创建和使用一个map#include <iostream> #include #include <string> #include <utility> // for pair and make_pair using namespace std; int main() { // 定义一个空的map对象 map<string, int> dictionary1; // dictionary中有三个元素 map<string, int> dictionary = { {"a", 1}, {"an", 2}, {"and", 3} }; // 下标操作 dictionary["an"] = 1; // 如果dictionary中没有键an，则将("an",1)插入容器中 // 向map中插入新元素 dictionary.insert(map<string, int>::value_type("aq", 2)); string s = "example"; // 更简单的方法：插入元素(s, 1) // dictionary.insert((s, 1)); // 错误写法 dictionary.insert(make_pair(s, 1)); dictionary.insert(pair<string, int>(s, 1)); // 定义map的迭代器 map<string, int>::iterator map_it = dictionary.begin(); // 更简单的写法 auto auto_map_it = dictionary.begin(); return 0; }对map进行操作int main() { // 使用map中元素的值（以输出为例）： map<string, int> dictionary = {{"a", 1}, {"an", 2}, {"and", 3}}; auto map_it = dictionary.begin(); cout << map_it->first; // 输出迭代器指向元素的键key cout << map_it->second; // 输出迭代器指向元素的值value cout << endl; // 删除map中元素： dictionary.erase("an"); // 删除键为"an"的元素 dictionary.erase(map_it); // 删除迭代器指向的元素 // 遍历map中元素： for(auto map_it = dictionary.begin(); map_it != dictionary.end(); map_it++) { cout << map_it->first << " " << map_it->second << endl; } // 直接用key作为下标来访问值value cout << dictionary["an"] << endl; // 输出键"an"对应的值，如果不存在则创建新元素（值初始化为0） // 另一个示例：遍历时删除元素 map<int, string> m = {{1, "a"}, {2, "b"}, {3, "c"}}; for (auto it = m.begin(); it != m.end(); ) { if (it->first % 2 == 0) { it = m.erase(it); // 删除偶数键元素，返回下一有效迭代器 } else { cout << it->second << ";"; // 输出奇数键对应的值 ++it; } } cout << endl; return 0; }pair类型头文件#include <utility> // std::pair 定义在此初始化定义// 方式1：直接初始化 std::pair<int, std::string> p1(1, "apple"); // 方式2：使用 make_pair（自动推导类型） auto p2 = std::make_pair(2, "banana"); // 方式3：C++17 起支持结构化绑定（C++17） auto [id, name] = std::make_pair(3, "cherry");std::pair<int, std::string> p(1, "apple"); std::cout << p.first << ", " << p.second << std::endl; // 输出: 1, apple比较操作（按 first 和 second 字典序）std::pair<int, int> a(1, 2); std::pair<int, int> b(1, 3); std::cout << (a < b) << std::endl; // 输出 1（true），因为 a.second < b.second交换两个 pairstd::pair<int, int> x(1, 2); std::pair<int, int> y(3, 4); x.swap(y); // 或 std::swap(x, y)赋值std::pair<int, std::string> p; p = std::make_pair(4, "orange"); // 赋值在 STL 容器中作为 std::map 或 std::unordered_map 的元素#include std::map<int, std::string> m; m.insert(std::make_pair(1, "apple")); // 插入 pair m.emplace(2, "banana"); // C++11 更高效的方式 // 遍历 map（每个元素是一个 pair） for (const auto& [key, value] : m) { // C++17 结构化绑定 std::cout << key << ": " << value << std::endl; }操作描述pair<T1, T2> p;p 是一个成员类型为 T1 和 T2 的 pair, 成员 first 和 second 都进行了值初始化pair<T1, T2> p(v1, v2);pair<T1, T2> p = {v1,v2};p 是一个成员类型为 T1 和 T2 的 pair, 成员 first 和 second 分别用 v1 和 v2 初始化make_pair(v1, v2);返回一个用 v1 和 v2 初始化的 pair。pair 的类型从 v1 和 v2 的类型推断出来p.first返回 p的 first 数据成员p.second返回 p 的 second 数据成员p1 relop p2关系运算符 relop(<, <=, >, >=) 按字典序定义：当 p1.first < p2.first !(p2.first<p1.first) && p1.second < p2.second 成立时，p1 < p2 为 true关系运算利用元素的 < 运算符实现p1 == p2p1 != p2当 first 和 second 成员分别相等时，两个 pair 相等。相等性判断利用元素的 == 运算符实现set类型set 是单纯键的集合，主要用于查询#include <iostream> #include <set> #include <string> using namespace std; int main() { // 定义set时需要指明键类型 set<string> sset; // 向set中插入元素 sset.insert("bee"); sset.insert("butterfly"); // 使用count()函数统计set中指定键的个数 // 返回1表示存在该元素，0表示该元素不存在 if (sset.count("butterfly")) { cout << "Set contains 'butterfly'" << endl; } else { cout << "Set does not contain 'butterfly'" << endl; } // 其他常用set操作示例 // 查找元素 auto it = sset.find("bee"); if (it != sset.end()) { cout << "Found 'bee' in the set" << endl; } // 遍历set中的元素 cout << "Set elements: "; for (const auto& elem : sset) { cout << elem << " "; } cout << endl; // 删除元素 sset.erase("bee"); cout << "After erasing 'bee', set size: " << sset.size() << endl; return 0; }5.泛型算法find()查找算法find(b,e,search_value) 函数查找容器中位于迭代器 b 和 e 间的值 search_value如果查找成功 find() 就 返回指向该元素的迭代器 ，否则返回第二个实参通过判断 find() 的 返回值与第二个实参是否相等 ，就可以得知查找是否成功find() 还可以用来在内置数组中查找指定元素#include <iostream> #include #include <algorithm> // 需要包含 algorithm 头文件以使用 find using namespace std; int main() { list<int> ilist; // 向 ilist 中插入元素（原代码缺少插入操作，这里补充示例） ilist.push_back(100); ilist.push_back(34); ilist.push_back(78); ilist.push_back(3); // 查找是否有 78 这个元素 auto result_it = find(ilist.begin(), ilist.end(), 78); // 判断查找是否成功 if (result_it == ilist.end()) { cout << "fail" << endl; } else { cout << "success" << endl; // 会输出 success } // find() 在内置数组中查找指定元素 int ia[6] = {100, 34, 78, 3}; // 后两个元素默认初始化为 0 int* pr = find(ia, ia + 6, 78); if (pr == ia + 6) { cout << "fail" << endl; } else { cout << "success" << endl; // 会输出 success } return 0; }sort()排序算法#include <algorithm> #include <vector> using namespace std; vector<int> nums = {3, 1, 4, 1, 5, 9}; sort(nums.begin(), nums.end()); // 默认升序：{1, 1, 3, 4, 5, 9}可以自定义排序逻辑// 降序排序 sort(nums.begin(), nums.end(), [](int a, int b) { return a > b; // 当 a > b 时，a 排在前面 }); // 自定义规则示例：按绝对值降序 sort(nums.begin(), nums.end(), [](int a, int b) { return abs(a) > abs(b); });
第八章 模板与泛型编程1.简介函数模板提供了一种描述通用算法的机制将算法处理的 数据类型参数化 ，从而既可以保留函数定义和函数调用的语义，又不必绕过 C++ 的类型检查程序员 将函数接口（参数表和返回类型）中的全部或者部分类型参数化 ，而函数体保持不变 。如果一个 函数的实现对一组实例都是相同的 ，区别仅在于每个实例处理不同的数据类型 ，那么该函数就可以定义为函数模板模板的定义方式template <typename T>2.应用用函数模板实现通用算法将具体算法实现中的 数据类型参数化 ，用 模板类型参数 代替，保留函数中与类型无关的代码，就可以得到函数模板的定义// 模板函数定义（修正原图中的语法错误） template <class Type> Type min(Type array[], int size) { Type min_val = array[0]; // 初始化最小值为首元素 for (int ix = 1; ix < size; ++ix) // 从第二个元素开始遍历 if (array[ix] < min_val) min_val = array[ix]; // 发现更小值时更新 return min_val; // 返回最小值 }使用模板非类型参数的算法实现模板类型参数 Type 在模板定义中作为类型标识符使用，表示数组的类型模板非类型参数 size 作为常量使用，表示数组的大小// 函数模板：返回定长数组的最小值（通过const引用传递数组） template <class Type, int size> Type min2(const Type (&array)[size]) { // 想传递数组的const引用 Type min_val = array[0]; // 初始化最小值为首元素 for (int ix = 1; ix < size; ++ix) // 遍历数组 if (array[ix] < min_val) min_val = array[ix]; // 更新最小值 return min_val; }3.函数模板的实例化函数模板本身并不是真正的函数定义，因为其中的类型信息不完整函数模板被调用时编译器首先对其进行实例化 ，生成真正整的函数定义实例，然后再 实施对函数实例的调用根据每次调用模板函数的不同实参，一个函数模板可以实例化得到多个不同的函数定义当调用一个函数模板时，编译器用 函数实参 推演 模板实参模板实例化在函数模板被调用或取地址时隐式发生// 版本1：接收数组指针和大小参数 template<class Type> Type min(Type array[], int size) { Type min_val = array[0]; for (int i = 1; i < size; ++i) if (array[i] < min_val) min_val = array[i]; return min_val; } // 版本2：接收数组的常量引用（编译期确定大小） template<class Type, int size> Type min2(const Type (&array)[size]) { Type min_val = array[0]; for (int i = 1; i < size; ++i) if (array[i] < min_val) min_val = array[i]; return min_val; }例如，当调用模板 min 时，编译器会用一个具体类型替换 Type调用模板 min2 时会用一个 具体类型 和一个 常量值 分别替换 Type 和 size在使用函数模板时，必须能够通过上下文为每一个模板实参确定 一个唯一的类型或值 ，否则会产生编译错误// 三类型参数的函数模板 template <class T1, class T2, class T3> T1 funcion(T2 arg1, T3 arg2) { /*...*/ } // 单类型参数的函数模板 template <class T> void goo(T a, T b) { /*...*/ } int main() { int x, y; double d; x = funcion(y, d); // 正确用法：x = funcion<int>(y, d); goo(x, d); // 正确用法：goo<double>(x, d); }显式指定模板实参在函数模板实例化时，函数名后面用尖括号<>指定模板实参函数实参可以应用隐式类型转换，转换成相应的函数参数显式模板实参应该只用在解决二义性时，或用在模板实参不能被推演出来的情况下// 三元模板函数定义 template <class T1, class T2, class T3> T1 function(T2 arg1, T3 arg2) { /*...*/ } int main() { int x, y; double d; // 三种调用方式示例 x = function(y, d); // 调用1：缺少T1信息（无法推导） x = function<int, int, double>(y, d); // 调用2：显式指定全部类型 x = function<int>(y, d); // 调用3：显式指定T1，推导T2和T3 }4.重载函数模板用函数模板重载另一个函数模板，函数参数的个数不同，而不是参数类型也可以用一个非模板函数重载一个同名的函数模板// 两个同类型参数的min模板 template<class Type> Type min(Type a, Type b) { return a < b ? a : b; } // 三个同类型参数的min模板 template<class Type> Type min(Type a, Type b, Type c) { Type t = a < b ? a : b; return t < c ? t : c; } // const char*特化版本（字符串比较） const char* min(const char* a, const char* b) { return strcmp(a, b) < 0 ? a : b; }具体运行逻辑如下：1.寻找一个参数完全匹配的非模板函数，若找到，则调用该函数2.寻找一个函数模板，能将其实例化为一个完全匹配的函数，若找到，则调用；3.只考虑重载函数集中的普通函数（会尝试进行隐式类型转换），按照重载函数解析过程选择可调用的函数，若找到，则调用。如果都没有找到匹配的函数，则出现“无可调用的函数”错误；如果某一步内出现多个匹配的函数，则引起二义性错误。5.类模板类模板用来实现通用数据类型,vector 就是 C++ 标准模板库中定义的一个类模板类模板定义的一般语法形式为：template <模板参数表>下例中的栈模板可以实现元素为不同类型的栈// 栈模板类实现（修正版） template<class Type> class Stack { public: Stack(int cap) : size(cap), top(bottom) { // 构造函数 ele = new Type[size]; // 动态分配内存 } ~Stack() { delete[] ele; } // 析构函数释放内存 Type pop() { // 弹出栈顶元素 if (empty()) throw "Stack Underflow"; // 空栈检查 return ele[top--]; // 返回并移动栈顶指针 } void push(Type e) { // 压入元素 if (full()) throw "Stack Overflow"; // 栈满检查 ele[++top] = e; // 移动指针后存储 } bool empty() { return top == bottom; } // 栈空判断（bottom=-1） bool full() { return top == size - 1; } // 栈满判断 private: Type* ele; // 元素存储数组 int top; // 栈顶指针（初始为-1） int size; // 栈容量 const static int bottom = -1; // 栈底标记 };类模板的实例化从类模板定义中生成类的过程称为模板实例化类模板本身并不定义任何类型，而只是指定了怎样根据一个或多个实际的类型或值构造单独的类不同于函数模板，类模板在 实例化时 必须显式指定 模板实参类模板的每个实例都是一个独立的类型Stack<int> is(20) Stack<string> ss(10)类模板的成员函数与非类型参数成员函数也可以在类模板之外定义，定义前面必须加关键字 template 以及模板参数表模板的非类型参数代表一个常量值（大多数情况下都是整型）template <class Type, int cap> class Stack { public: Stack(); ~Stack() { delete []ele; } void push(Type e) { ele[++top] = e; } Type pop() { return ele[top--]; } bool empty() { return top == bottom; } bool full() { return top == size-1; } private: Type* ele; int top; int size; const static int bottom = -1; }; // 类外定义 template<class Type, int cap> Stack<Type, cap>::Stack() { assert(cap > 0); size = cap; ele = new Type[size]; top = bottom; } int main() { Stack<int, 10> is; for(int i = 0; i < 10; i++) is.push(i); while(!is.empty()) cout << is.pop() << "\t"; }类模板的静态数据成员类模板中可以声明 static 数据成员， static 数据成员的定义必须出现在类模板的定义之外类模板的每个实例都有自己的一组 static 数据成员每个 static 数据成员实例都与一个类模板实例相对应因此，一个 static 数据成员的实例总是通过一个特定的类模板实例被引用类模板的友元1.非模板友元类或友元函数不使用模板参数的友元类或友元函数是类模板的所有实例的友元2.绑定的友元类模板或函数模板使用模板类的模板参数的友元类和友元函数与实例化后的模板类实例之间是一一对应的关系下面程序中的 Queue 就是 QueueItem 的友元类模板，通过模板类型参数绑定。template <class Type> class Queue; // 向前引用声明Queue template <class T> class QueueItem { public: QueueItem(const T& data) : item(data), next(0) {} friend class Queue<T>;//友元声明，使用模板类型参数 T 绑定实例 private: T item; QueueItem* next; }; template <class Type> class Queue { public: Queue() : front(0), back(0) {} ~Queue(); Type remove(); void add(const Type&); bool is_empty() const { return front == 0; } private: QueueItem<Type>* front; QueueItem<Type>* back; }; template <class Type> void Queue<Type>::add(const Type& val) { // 实现代码 } template <class Type> Type Queue<Type>::remove() { // 实现代码 }3.非绑定的友元类模板或函数模板友元类模板或函数模板有自己的模板参数，和模板类实例之间形成一对多的映射关系对任一个模板类的实例，友元类模板或函数模板的所有实例都是它的友元template <class T> class MTC { public: friend void foo(); // 友元函数与模板类实例之间是一对多关系 // foo是所有MTC模板的实例的友元 friend void goo<T>(vector<T>); // 友元函数实例与模板类实例之间是一对一关系 // 每个实例化的MTC类都有一个相应的goo友元实例 template <class Type> friend void hoo(MTC<Type>); // 友元函数实例与单个模板类实例之间是多对一关系 // 对MTC的每个实例，hoo的每个实例都是它的友元 };模板的代码组织为了避免模板定义被同一文件多次包含而引起编译错误，在模板定义的头文件中应该使用包含#ifndef FLAG #define FLAG //模板定义 #endif// 模板定义头文件 templatemin.h #ifndef TEMPLATE_MIN_H #define TEMPLATE_MIN_H template <class Type> Type min(Type array[], int size) { Type min_val = array[0]; for (int ix = 1; ix < size; ++ix) if (array[ix] < min_val) min_val = array[ix]; return min_val; } #endif /* TEMPLATE_MIN_H */ // 使用模板定义的源文件 test.cpp #include <iostream> #include "templatemin.h" using namespace std; int main() { int a[] = {2, 5, 3, 4, 1, 7}; cout << "min(a,6) = " << min(a, 6) << endl; double da[] = {1.2, 3.5, 7.2}; cout << "min(da,3) = " << min(da, 3) << endl; }
第七章 输入输出流类库1.简介C++语言的 I/O 系统向用户提供一个统一的接口，使得程序的设计尽量与所访问的具体设备无关，在用户与设备之间提供了一个抽象的界面：输入输出流用标准流进行输入输出时，系统自动地完成数据类型的转换。 对于输入流，要将输入的字符序列形式的数据变换成计算机内部形式的数据（二进制或 ASCII）后，再赋给变量变换后的格式由变量的类型确定。对于输出流，将要输出的数据变换成字符串形式后，送到输出流（文件）中。2.重载输入输出运算符自定义的对象不能直接输入输出，需要对运算符 << >> 进行重载重载输入运算符在对象所在的类中 将重载这二个运算符的函数说明该类的 友元函数 。friend istream & operater >>(istream&, ClassName& ); // 在类内的声明 istream & operater >>(istream &is , A &a) // 在类外的声明 cin >> a; // 调用1 operator >> (cin,a) // 调用2，效果同上 其中 istream 作为返回值类型， operator作为函数名istream&作为左操作数，ClassName&作为右操作数返回值类型：类 istream 的引用， cin 中可以连续使用运算符cin >> a >> b;class incount { int c1, c2; public: incount(int a = 0, int b = 0) { c1 = a; c2 = b; } void show(void) { cout << "c1=" << c1 << "\t" << "c2=" << c2 << endl; } friend istream& operator>>(istream&, incount&);// 重载输入函数声明 }; istream& operator>>(istream& is, incount& cc) { is >> cc.c1 >> cc.c2; return is; } void main(void) { incount x1, x2; x1.show(); x2.show(); cin >> x1; cin >> x2; x1.show(); x2.show(); }重载输出运算符#include <iostream> using namespace std; class incount { int c1, c2; public: incount(int a = 0, int b = 0) { c1 = a; c2 = b; } void show(void) { cout << "c1=" << c1 << "\t" << "c2=" << c2 << endl; } friend istream& operator>>(istream&, incount&); friend ostream& operator<<(ostream&, incount&); }; // 重载输入函数定义 istream& operator>>(istream& is, incount& cc) { is >> cc.c1 >> cc.c2; return is; } // 重载输出函数定义 ostream& operator<<(ostream& os, incount& cc) { os << "c1=" << cc.c1 << "\t" << "c2=" << cc.c2 << endl; return os; } int main() { incount x1, x2; cout << x1 << x2; // 调用输出函数 cin >> x1; // 调用输入函数 cin >> x2; cout << x1 << x2; return 0; }3.文件流C++在头文件 fstream.h 中定义了 C++ 的文件流类体系文件分为使用 ASCII 和 二进制读取 两种方式文本文件的打开与关闭在文件操作前，需要将程序与被操作的文件联系起来使程序可以引用文件在程序内定义一个文件类的对象，由该对象与文件发生联系，程序内所有的与文件的操作都是对该对象的操作 。fstream infile ,outfile; // 两个对象，可以联系两个输入输出文件 ifstream infile; // 输入 ofstream outfile; // 输出ifstream infile； infile.open(“myfile1.txt”)打开文件 myfile1.txt 用于输入，并将这个文件与输入文件类对象 infile 建立联系在程序中，用到这个文件 myfile1.txt 的地方就用 infile来代替。ifstream infile; ofstream outfile; infile.open("myfile1.txt"); outfile.open("myfile2.txt"); float x, y; infile >> x >> y; // 从文件读取 outfile << x << 't' << y << endl; // 输出到文件 infile.close(); // 使用完则关闭文档 outfile.close();// 使用完则关闭文档使用 fstream 进行读写当用类 fstream 定义文件对象时必须在成员函数 open() 中的参数中 给出打开方式（读或写）pfile1.open(“file1.txt”,ios::in );//pfile1 联系到“ file1.txt”, 用于输入 pfile2.open(“file2.txt”,ios::out );//pfile2 联系到“ file2.txt”, 用于输出下文展示如何具体写入和读出数据到文件中#include <iostream> #include <fstream> using namespace std; int main() { int a[10]; ifstream infile; // 定义输入文件流对象 ofstream outfile; // 定义输出文件流对象 infile.open("file1.txt"); // 打开输入文件"file1.txt" outfile.open("file2.txt"); // 打开输出文件"file2.txt" if (!infile) { cout << "无法打开输入文件: file1.txt" << endl; return 1; } if (!outfile) { cout << "无法打开输出文件: file2.txt" << endl; return 1; } for (int i = 0; i < 10; i++) { infile >> a[i]; // 从"file1.txt"读取十个整数到数组a } for (int i = 0; i < 10; i++) { outfile << a[i] << "\t"; // 将数组a的十个数写入"file2.txt"，用制表符分隔 } outfile << endl; // 写入换行符 infile.close(); // 关闭输入文件 outfile.close(); // 关闭输出文件 return 0; }判断打开成功在打开文件后，都要判断打开是否成功。若打开成功，则文件流对象值为非零值； 若打开不成功，则其值为 0打开输入文件时，文件必须存在打开输出文件时，若文件不存在，则建立文件；若文件存在，则删除原文件的内容，使其成为一个空文件#include <iostream> #include <fstream> #include <cstdlib> // for exit() using namespace std; int main() { fstream pfile1, pfile2; // 定义了两个文件类的对象 pfile1.open("file1.txt", ios::in); pfile2.open("file2.txt", ios::out); if (!pfile1) { // 若为0，打开文件操作失败 cout << "不能打开输入文件：file1.txt" << '\n'; exit(1); } if (!pfile2) { cout << "不能打开输出文件：file2.txt" << '\n'; exit(1); } // 文件操作代码... pfile1.close(); pfile2.close(); return 0; }常见字符串读写参见下方，类似于 cin 和 cout 的使用cin.get(ch); //从键盘输入一个字符 infile.get(ch); //从文件输入一个字符 cout.put(ch); //向显示器输出一个字符 outfile.put(ch); //向文件输出一个字符 cin.getline(str,300); //从键盘输入一行字符 infile.getline(str,300);//从文件输入一行字符二进制读写操作fstream infile,outfile; infile.open(“inf1.dat”, ios::in|ios::binary); // 使用二进制输入方式打开 outfile.open(“outf1.dat”, ios::out|ios::binary);由于二进制文件中的数据不是 ASCII 码，故不能直接对其读写， 必须要通过特定的函数予以转换具体使用时一般使用 char 类型转换// 读取单个整数 int i; infile.read(reinterpret_cast<char*>(&i), sizeof(int)); // 这里使用了强制类型转换：reinterpret_cast<char*>，该方式不做任何的检查 // 读取整数数组 int a[10]; infile.read(reinterpret_cast<char*>(a), 10 * sizeof(int)); // 写入一个整数 int i = 4; outfile.write(reinterpret_cast<char*>(&i), sizeof(int)); // 写入一个数组 int a[10]={0,1,2,3,4,5,6,7,8,9}; outfile.write(reinterpret_cast<char*a, 10 * sizeof(int));在二进制的读取中，判断是否读到文件尾部使用 infile.eof()当到达文件结束位置时，该函数返回一个非零值，否则返回零。fstream infile; infile.open("data1.dat",ios::in|ios::binary); if(!infile) cout<<"Open Error!\n"; exit(1); char str[300]; while(!infile.eof()) // 没有结束就继续读取 infile.read(str, 300);完整的代码示例#include <iostream> #include <fstream> #include <cstring> // For strcpy if needed using namespace std; int main() { char filename1[256], filename2[256]; char buff[4096]; cout << "输入源文件名: "; cin >> filename1; cout << "输入目的文件名: "; cin >> filename2; fstream infile, outfile; infile.open(filename1, ios::in | ios::binary); if (!infile) { cerr << "无法打开源文件: " << filename1 << endl; return 1; } outfile.open(filename2, ios::out | ios::binary); if (!outfile) { cerr << "无法创建目标文件: " << filename2 << endl; infile.close(); return 1; } while (true) { infile.read(buff, sizeof(buff)); streamsize n = infile.gcount(); // 获取实际读取的字节数 if (n > 0) { outfile.write(buff, n); } if (infile.eof()) { break; // 到达文件末尾，退出循环 } if (!infile) { cerr << "文件读取错误" << endl; break; } } infile.close(); outfile.close(); cout << "文件复制完成" << endl; return 0; }
第六章 运算符重载1.函数的重载所谓函数的重载是指完成不同功能的函数可以具有相同的函数名 C++的编译器是根据 函数的实参 来确定应该调用哪一个函数，仅返回值不同时，不能定义为重载函数2.运算符的重载重载的概念在下例中，编译系统中的运算符本身不能做这种运算若使下式可以运算，必须重新定义运算符，这种重新定义的过程成为运算符的重载。char str[4], c1[2]="a", c2[2]="b"; str= c1 + c2;class A{ float x, y; public: A(float a = 0, float b = 0){ x = a; y = b; } }; void main(void){ A a(2, 3), b(3, 4), c; c = a + b; // 两对象不能使用＋，必须重新定义＋ }运算符重载就是赋予已有的运算符多重含义C++ 通过重新定义运算符，使它能够用于特定类的对象执行特定的功能多态性的体现运算符的重载从另一个方面体现了OOP 技术的多态性， 且同一运算符根据不同的运算对象可以完成不同的操作。为了重载运算符，必须定义一个函数，并告诉编译器， 遇到这个 重载运算符 就调用该函数，由这个函数来完成该运算符应该完成的操作。 这种函数称为运算符重载函数，它通常是 类的成员函数或者是 友元函数。 运算符的操作数通常也应该是类的对象。ClassName operator <operator> (<args>){;} A operator + (A&); //重载了类 A 的 + 运算符operator 是定义运算符重载函数的关键字，与其后的运算符一起构成函数名。#include <iostream> using namespace std; class A{ int i; public: A(int a=0) { i = a; } void Show(void) { cout << "i=" << i << endl; } void AddA(A &a, A &b) // 利用函数进行类之间的运算 { i = a.i + b.i; } A operator+(A &a) // 重载运算符+ { A t; t.i = i + a.i; return t; } }; int main(){ A a1(10), a2(20), a3; a1.Show(); a2.Show(); a3 = a1 + a2; // 重新解释了加法，可以直接进行类的运算 a3.AddA(a1, a2); // 调用专门的功能函数 a3.Show(); return 0; }重载运算符限制重载运算符只能是两种参数情况：没有参数或带有一个参数对于只有一个操作数的运算符 如++ 在重载这种运算符时通常不能有参数；而对于有二个操作数的运算符只能带有一个参数 。 这参数可以是对象对象的引用 或其它类型的参数 。 在 C++ 中不允许重载有三个操作数的运算符只能对 C++ 中已定义了的运算符进行重载，当重载一个运算符时该运算符的优先级和结合律是不能改变的不能进行重载的运算符有6个，见下表名称符号成员访问运算符.成员指针访问运算符.* 和 ->*作用域运算符::大小运算符sizeof三目运算符? :3.前置后置运算符重载只具有一个操作数的运算符为单目运算符，最常用的为 ++ 及 --可以看出，虽然运算后对象的值一致，但先自加或后自加的重载运算符函数的返回值不一致， 必须在重载时予以区分<type> operator ++() {;} // 前置运算 <type> operator ++(int) {;} // 后置运算下面是一段可以直接运行的代码注意返回 *this 会返回一个对象的副本，并非原来的对象根据 return 返回的值可以区分前置和后置运算符#include <iostream> using namespace std; class incount { int c1, c2; public: incount(int a = 0, int b = 0) { c1 = a; c2 = b; } void Show(void) { cout << "c1=" << c1 << '\t' << "c2=" << c2 << endl; } incount operator++() { c1++; c2++; return *this; // 返回拷贝的副本，并非本身 } // 前置 incount operator++(int) // 后置 { incount c; c.c1 = c1; c.c2 = c2; c1++; c2++; return c; } }; int main() { incount ic1(10, 20), ic2(100, 200), ic3, ic4; ic1.Show(); ic2.Show(); ic3 = ++ic1; // ic3=ic1.operator++() ic4 = ic2++; // ic4=ic4.operator++(3) ic3.Show(); ic4.Show(); return 0; }4.运算符重载为友元函数如果运算符重载为成员函数时，是由一个操作数调用另一个操作数用成员函数实现运算符的重载时运算符的左操作数为当前对象，并且要用到隐含的 this 指针运算符重载函数不能定义为静态的成员函数，因为静态的成员函数中没有 this 指针c = a + b; // 左操作数是a c = a.operator+(b); // 本质上是这样 c = ++a; c = a.operator++(); c += a; c = a.operator++(a);友元函数是在类外的普通函数，与一般函数的区别是可以调用类中的私有或保护数据将运算符的重载函数定义为友元函数，参与运算的对象全部成为函数参数c = a + b // 实际上是 c = operator+(a, b) c = ++a; // 实际上是c = operator++(a) c += a; // 实际上是 operator+=(c, a);对双目运算符，友元函数有 2 个参数；对单目运算符，友元函数有 1 个参数。有些运算符不能重载为友元函数: ＝, ()，[]使用格式如下friend <type> operator<operator> (<args>) {;} c = a + b; // c = operator+(a, b); friend A operator+ (A &a, A &b) {;}class A{ int i; public: A(int a=0) { i = a; } void Show(void) { cout << "i=" << i << endl; } friend A operator+(A &, A &); // 友元函数，两个参数，为引用 }; A operator+(A &a, A &b){ A t; t.i = a.i + b.i; return t; } int main(){ A a1(10), a2(20), a3; a1.Show(); // 相当于 a3=operator+(a1,a2) a2.Show(); a3 = a1 + a2; // 重新解释了加法，可以直接进行类的运算 a3.Show(); return 0; }前后单目运算符// ++为前置运算时 它的运算符重载函数的一般格式为： A operator ++(A &a) {;} //++为后置运算时 它的运算符重载函数的一般格式为： A operator ++(A & a,int) {;}对双目运算符，重载为成员函数时，仅一个参数，另一个被隐含；重载为友元函数时，有两个参数，没有隐含参数。一般来说，单目运算符最好被重载为成员函数；双目运算符最好被重载友元函数5.赋值运算符的重载同类型的对象间可以相互赋值，等同于对象的各个成员的一一赋值。A a(2,3), b; b = a;但当对象的成员中使用了动态的数据类型时（用 new 开辟空间），不能直接相互赋值，否则在程序的执行期间会出现运行错误class A { char *ps; public: A() { ps = 0; } A(char *s) { ps = new char[strlen(s) + 1]; strcpy(ps, s); } ~A() { if (ps) delete ps; } void Show(void) { cout << ps << endl; } }; void main(void) { A s1("China!"), s2("Computer!"); s1.Show(); s2.Show(); s2 = s1; //相当于 s2.ps = s1.ps s1.Show(); s2.Show(); // 析构会出现问题 }这个需要使用到重载赋值运算符class A{ char *ps; public: A() { ps = 0; } A(char *s){ ps = new char[strlen(s) + 1]; strcpy(ps, s); } ~A() { if (ps) delete ps;} void Show(void) { cout << ps << endl; } A &operator=(A &b); // 声明重载运算符 }; A &A::operator=(A &b) // 重载赋值运算符 { if (ps) delete[] ps; // 先删除左操作的指针 if (b.ps){ ps = new char[strlen(b.ps) + 1]; // 创建一个新长度的空间 strcpy(ps, b.ps); } else ps = 0; return *this; // 返回这个类本身 } void main(void){ A s1("China!"), s2("Computer!"); s1.Show(); s2.Show(); s2 = s1; s1.Show(); s2.Show(); }6.选择是否成员重载赋值 = 、下标 [ ]、函数调用 ( ) , 成员函数访问箭头运算符 ->必须是成员函数复合赋值运算符 += 一般应该是成员，但并非必须改变对象状态的运算符或者与给定类型密切相关的运算符，如自增、自减和解引用运算符 *，通常应该是成员具有对称性的运算符可能转换两个操作数中的任何一个，如算术、关系和位运算符等，通常应该是友元函数重载移位运算符和用于对象的 I/O 操作时，左操作数是标准库流对象，右操作数才是类类型的对象， 只能用友元函数如果左操作数不是对象时，只能通过友元函数来实现class Complex{ public: Complex operator+(double); Complex operator=(Complex); friend Complex operator +(double,Complex); // 友元重载，左操作数是一个double类型 }; int main(){ Complex c1,c2,c3; c3 = c1 + 1.1; // 先调用成员函数+，再调用成员函数= c3 = 1.1 + c1; // 先调用友元函数+，再调用成员函数= }7.用户定义的类型转换补充：static_cast 运算符在C++中，static_cast 是一种显式类型转换运算符，用于在编译时进行类型转换（相对于 dynamic_cast的运行时类型转换）。它是C++中最常用的类型转换方式之一，比C风格的强制类型转换更安全，因为它会进行一些编译时检查。主要作用：基本数据类型之间的转换（如 int 转 double，float 转 int 等）指针/引用在类层次结构中的向上转换（派生类指针/引用 → 基类指针/引用）void指针与其他指针类型的互转显式调用构造函数或转换函数其他类型转换为本类对象带单个实参的构造函数提供了参数类型的对象到本类型对象的转换下例中的构造函数提供了从 int 类型转为 A 类型的转换class A{ int x; public: A(int b): x(b){;} };本类的对象转换为其他类型类型转换运算符 operator <type> ()只能用成员函数重载，不带参数，不必指定返回类型对当前对象实施类型转换操作，产生 <type> 类型的新对象#include <iostream> #include <cassert> using namespace std; class MinInt { char m; public: MinInt(int val = 0) { // int类型转换为MinInt，也是构造函数 assert(val >= 0 && val <= 100); // 要求取值范围在0～100之间 m = static_cast<char>(val); } operator int() { // MinInt转为int return static_cast<int>(m); } }; int main() { MinInt mi(10), num; num = mi + 20; int val = num; // 将num自动转换为int，并赋值给val cout << mi << "\t" << num << "\t" << val; }显式类型转换运算符C++11 标准引入了显式类型转换运算符 explicit ，编译器不会将显式类型转换运算符用于隐式类型转换class SmallInt { char val = 0; public: SmallInt(char v) : val(v) {} explicit operator int() const { return val; } }; SmallInt si = 3; // 隐式构造函数调用 si + 3; // 错误：需要隐式类型转换（但operator int是explicit的） static_cast<int>(si) + 3; // 正确：显式类型转换8.函数调用运算符如果类重载了函数调用运算符 operator()，就可以像用函数一样使用该类的对象函数调用运算符是唯一不限制操作数个数的运算符函数调用运算符必须是成员函数一个类可以定义多个不同版本的调用运算符，但是必须在参数个数或类型上有所区别函数对象如果一个类定义了 函数调用运算符 ，那么该类的对象称为函数对象 ，或者仿函数 (functor)因为可以调用这种对象，在代码层面感觉跟函数的使用一样，但本质上并非函数函数对象类除了 operator() 之外还可以包含其他成员，其中的数据成员可以用于定制调用运算符中的操作在定义对象时， 初始化对象的数据成员，这些数据成员的状态就成为函数对象的初始状态 。因此，可以借由函数对象产生多个功能类似却不相同的仿函数实例使用函数调用运算符的方式是令对象作用于一个实参列表 ，形式类似于函数调用 struct函数对象示例1通过带状态的函数对象，设定不同税率的计算在下例子中，创建了两个对象用于计算税，但具体的税率并不相同，这是通过创建对象时决定的class Tax { double rate; int base; public: Tax(double r, int b) : rate(r), base(b) {} double operator()(double money) { return (money - base) * rate; } }; int main() { Tax high(0.4, 30000); // 第一种税率计算 Tax middle(0.25, 20000); // 第二种税率计算 cout << "tax over 3w: " << high(38900) << endl; cout << "tax over 2w: " << middle(27500) << endl; return 0; }函数对象示例2带状态的函数对象，设定不同的输出流对象和分隔符#include <iostream> #include <string> using namespace std; class PrintString { ostream &os; // 输出流的引用（可以是 cout、cerr 或任何 ostream） char sep; // 分隔符 public: // 构造函数，默认使用 cout 和空格分隔符 PrintString(ostream &o = cout, char c = ' ') : os(o), sep(c) {} // 重载 () 运算符，使其可以像函数一样调用 void operator()(const string& str) { os << str << sep; // 输出字符串 + 分隔符 } }; int main() { PrintString welcomeMsg; // 默认使用 cout 和 ' ' 分隔符 welcomeMsg("Welcome!"); // 输出 "Welcome! " PrintString errMsg(cerr, '\n'); // 使用 cerr 和换行符 '\n' errMsg("Error!"); // 输出 "Error!\n" return 0; }
第五章 虚函数和多态性多态性：调用同一个函数名，可以根据需要但实现不同的功能。编译时的多态性是函数重载，运行时的多态性使用虚函数实现运行时的多态性是指在程序执行之前，根据函数名和参数无法确定应该调用哪一个函数，必须在程序的执行过程中，根据具体的执行情况来动态地确定1.基类指针访问基类函数可以将一个派生类对象的地址赋给基类的指针变量basep 只能引用从基类继承来的成员。Base *basep; basep = &b // 基类地址 basep = &d; // 派生类地址下例中，使用指针调用的是基类中有的公有函数class Point{ float x, y; public: Point() {} Point(float i, float j){ x = i; y = j; } float area(void) {return 0.0;} }; const float Pi = 3.14159; class Circle : public Point{ // 类 Point 的派生类 float radius; public: Circle(float r){radius = r;} float area(void) {return Pi * radius * radius;} }; void main(void){ Point *pp; // 基类指针，可以将派生类对象的地址赋给基类指针 Circle c(5.4321); pp = &c; cout << pp->area() << // 调用的是基类中有的公有函数 }2.基类指针访问派生类函数若要访问派生类中相同名字的函数，必须将基类中的同名函数定义为虚函数 将不同的派生类对象的地址赋给基类的指针变量后，就可以动态地根据这种赋值语句调用不同类中的函数 。实现动态联编方式的前提：1.先要声明虚函数2.类之间满足赋值兼容规则3.通过指针与引用来调用虚函数将 area() 声明为虚函数，编译器对其进行动态联编，按照实际对象 c 调用了 Circle 中的函数 area() 。使 Point 类中的 area() 与 Circle 类中的 area() 有一个统一的接口。class Point{ float x, y; public: Point() {} Point(float i, float j){ x = i; y = j; } virtual float area(void) {return 0.0;} // 首先声明虚函数 }; const float Pi = 3.14159; class Circle : public Point // 类 Point 的派生类 { float radius; public: Circle(float r){radius = r;} float area(void) {return Pi * radius * radius;} // 虚函数再定义 }; void main(void){ Point *pp; Circle c(5.4321); pp = &c; cout << pp->area() << // 调用的是派生类中的函数 }一旦把基类的成员函数定义为虚函数，由基类所派生出来的所有派生类中，该函数均保持虚函数的特性在派生类中重新定义基类中的虚函数时，可不用关键字 virtual 来修饰这个成员函数注意：1.当在基类中把成员函数定义为虚函数后，在其派生类中定义的虚函数必须与基类中的虚函数同名 参数的类型 、顺序 、参数的个数必须一一对应，函数的返回的类型也相同 ，若函数名相同，但参数的个数不同或者参数的类型不同时，则属于函数的隐藏而不是虚函数 2.实现这种动态的多态性时，必须使用基类类型的指针变量 ，并使该指针指向不同的派生类对象并通过调用指针所指向的虚函数才能实现动态的多态性。3.虚函数必须是类的一个成员函数，不能是友元函数，也不能是静态的成员函数。4.在派生类中 没有重新定义虚函数时，与一般的成员函数一样，当调用这种派生类对象的虚函数时， 则调用其基类中的虚函数 。5.可把析构函数定义为虚函数，不能将构造函数定义为虚函数6.虚函数与一般的成员函数相比较调用时的执行速度要慢一些为了实现多态性，在每一个派生类中均要保存相应虚函数的入口地址表，函数的调用机制也是间接实现的因此除了要编写一些通用的程序 并一定要使用虚函数才能完成其功能要求外，通常不必使用虚函数7.一个函数如果被定义成虚函数，则不管经历多少次派生，仍将保持其虚特性，以实现“一个接口，多个形态”。关键字: overrideC++11的关键字 override 用来标记派生类中的 虚函数 ，使程序员覆盖基类虚函数的意图更清晰如果覆盖时不小心写错了形参列表，编译器会报告错误class B{ virtual void f1() const; virtual void f2 (int); void f3(); }; class D1 : public B{ void f1() const override; // 正确： f1 与基类的 f1 匹配 void f2() override; // 错误：参数类型不匹配，不是覆盖 void f2(); // 正确：参数表不同，隐藏基类的 f2 void f3() override; // 错误： f3 不是虚函数 void f4() override; // 错误： B 中没有 f4 };3.纯虚函数在基类中不对虚函数给出有意义的实现，它只是在派生类中有具体的意义。这个虚函数称为纯虚函数 。含有纯虚函数的类是抽象类，不能定义对象注意：1.在定义纯虚函数时 不能定义虚函数的实现部分。2.把函数名赋于 0 本质上是将指向函数体的指针值赋为初值 0 ，在没有重新定义这种纯虚函数之前是不能调用这种函数的 3.把至少包含一个纯虚函数的类，称为抽象类。这种类只能作为派生类的基类，不能用来说明这种类的对象4.在以抽象类作为基类的派生类中必须有纯虚函数的实现部分，否则不可以产生对象抽象类的唯一用途是为派生类提供基类，纯虚函数作用是作为派生类中的成员函数的基础并实现动态多态性 。4.运行时类别识别(RTTI)利用虚函数实现运行时的多态性基类指针 可以访问派生类同名的成员函数基类指针 不能访问派生类增加的成员函数class employee { public: virtual void salary(); }; class manager : public employee { public: void salary(); }; class programmer : public employee { public: void salary(); void bonus(); }; int main(){ manager Harry; employee *pe=&Harry; pe -> salary(); programmer Ron; pe=&Ron; pe -> salary(); pe -> bonus(); //Error，基类指针不能访问派生类新增的成员 return 0; }5.基类向派生类的指针转换基类指针到派生类指针的类型转换安全吗？基类指针指向派生类对象时，向下类型转换是安全的；基类指针指向基类对象或者其他派生类的对象 ，那么不安全dynamic_cast 与向下类型转换显式类型转换运算符 dynamic_cast 操作数类型必须是带有一个或多个虚函数的类把一个类类型对象的指针转换成同一类层次结构中的其他类的指针把一个类类型对象的左值转换为同一类层次结构中其他类的引用dynamic_cast 运行时异常如果指针或左值操作数不能被安全地转换为目标类型，则 dynamic_cast 将失败如果是对 指针 类型的 dynamic_cast 失败，则结果是空指针如果针对 引用 类型的 dynamic_cast 失败，则抛出一个 bad_cast 类型的异常dynamic_cast 先检验所请求的转换是否有效，只有在有效时才会执行转换，而检验过程发生在程序运行时dynamic_cast 用于基类指针到派生类指针的安全转换，它被称为安全的向下类型转换。如果必须通过基类指针（或引用）使用派生类的特性，而该特性又没有出现在基类接口中时，可以使用 dynamic_cast 。使用 dynamic_cast 时注意：必须对一个含有 虚函数 的类层次进行操作结果必须 在检测是否为 0 之后才能使用class employee{ public: virtual void salary(); // 虚函数 }; class manager : public employee{ public: void salary(); }; class programmer : public employee{ public: void salary(); void bonus(); // 多一个奖金 }; void payroll(employee *pe){ pe->salary(); // 调用salary()函数 } void payroll(employee *pe){ programmer *pm = dynamic_cast<programmer *>(pe); // 向下转化指针为派生类 if (pm){ pm->bonus(); // 非空，允许调用bonus() } pe->salary(); }在上例中，使用programmer *pm = dynamic_cast<programmer *>(pe);语句将 employee 类型的指针转化为 programmer 类型指针，使用的是 dynamic_casttypeid 运算符typeid 运算符对类型或表达式进行操作，返回操作数的确切类型typeid 运算符返回一个 type_info 类型的引用，type_info 在头文件 typeinfo.h 中定义，type_info::name() 返回 C 风格字符串表示的类型名typeid 运算符必须与表达式或类型名一起使用当 typeid 运算符的操作数是类类型，但不是带有虚函数的类类型时，typeid 运算符会指出操作数的类型，而不是底层对象的类型#include <typeinfo> class Base{ /*没有虚函数*/ }; class Derived : public Base { /*没有虚函数*/ }; void main() { Derived dobj; Base* pb = &dobj; cout << typeid(*pb).name() << endl; // 结果是Base }#include <typeinfo> class Base{ /*含有虚函数*/ }; class Derived : public Base { /*含有虚函数*/ }; void main() { Derived dobj; Base* pb = &dobj; cout << typeid(*pb).name() << endl; // 结果是Derived }
第四章 类的其他特性1.友元函数友元函数是一种定义在类外部的普通函数，能够访问类中私有成员和保护成员。友元函数需要在类体内进行说明，在前面加上关键字 friendfriend <type> FuncName(<args>);友元函数不是成员函数，用法也与普通的函数完全一致友元函数破坏了类的封装性和隐蔽性，使得非成员函数可以访问类的私有成员class A{ float x, y; public: A(float a, float b){ x = a; y = b; } float Sum() { return x + y; } friend float Sum(A &a) { return a.x + a.y; } // 必须使用类名引用来访问，因为友元函数不是类成员 }; void main(void){ A t1(4, 5), t2(10, 20); cout << t1.Sum() << endl; cout << Sum(t2) << endl; }类与类的沟通大多数情况是友元函数是某个类的成员函数，即A类中的某个成员函数是B类中的友元函数，这个成员函数可以直接访问B类中的私有数据。这就实现了类与类之间的沟通。下例中，在 B 中声明友元，在 A 中声明成员， 在类外定义函数 A::fun()class B; // 先定义类A，则首先对类B作引用性说明 class A{ // 类A的成员定义 public: void fun(B &); // 函数的原型说明 }; class B{ friend void A::fun(B &); // 定义友元函数 } void A::fun(B &b) {} // 函数的完整定义2.友元类可以在一个类中声明另一个类是其的友元类const float PI = 3.1415926; class A{ float r; float h; public: A(float a, float b){ r = a; h = b; } float Getr() { return r; } float Geth() { return h; } friend class B; // 定义类B为类A的友元 }; class B{ int number; public: B(int n = 1) { number = n; } void Show(A &a){ // 需要使用A的对象名来访问 cout << PI * a.r * a.r * a.h * number << endl; } // 求类A的某个对象*n的体积 }; void main(void){ A a1(25, 40), a2(10, 40); B b1(2); b1.Show(a1); b1.Show(a2); }3.派生类访问基类私有成员不管是按哪一种方式派生，基类的私有成员在派生类中都是不可见的。如果在一个派生类中要访问基类中的私有成员，可以将这个派生类声明为基类的友元。class M{ friend class N; // N为M的友元，可以直接使用M中的私有成员 private: int i, j; void show(void) { cout << "i=" << i << '\t' << "j=" << j << '\t'; } public: M(int a = 0, int b = 0){ i = a; j = b; } }; class N : public M{ // N为M的派生类 public: N(int a = 0, int b = 0) : M(a, b) {} void Print(void){ show(); cout << "i+j=" << i + j << endl; } }; void main(void){ N n1(10, 20); M m1(100, 200); // m1.show(); //私有成员函数，在类外不可调用 n1.Print(); }4.静态成员类所产生的所有对象中的静态成员均共享一个存储空间，这个空间是在编译的时候分配的。静态数据成员在类定义中，用关键字 static 修饰的数据成员称为静态数据成员。必须在文件作用域中，对静态数据成员作一次且只能作一次定义性说明可以通过静态数据成员名前加上类名和作用域运算符，可直接引用静态数据成员class A{ int x,y; static int z; public: void Setxy(int a, int b){ x=a; y=b;} }; A a1, a2; // 共用一个z int A::z = 0; // 必须对静态成员作一次定义性说明 静态成员函数可以将类的成员函数定义为静态的成员函数。即使用关键字 static 来修饰成员函数1.与静态数据成员一样，在类外的程序代码中，通过类名加上作用域操作符，可直接调用静态成员函数。2.静态成员函数只能直接使用本类的静态数据成员或静态成员函数，但不能直接使用非静态的数据成员（可以通过对象名引用使用）。这是因为静态成员函数可被其它程序代码直接调用，所以，它不包含对象地址的 this 指针。3.静态成员函数的实现部分在类定义之外定义时，其前面不能加修饰词 static 。这是由于关键字 static 不是数据类型的组成部分，因此，在类外定义静态成员函数的实现部分时，不能使用这个关键字4.不能把静态成员函数定义为虚函数。静态成员函数也是在编译时分配存储空间，所以在程序的执行过程中不能提供多态性。5.可将静态成员函数定义为内联的（inline），其定义方法与非静态成员函数完全相同。class Tc{ private: int A; static int B; // 静态数据成员 public: Tc(int a){ A = a; B += a; } static void display(Tc c); // Tc的对象为形参 }; void TC::display(Tc c){ cout << "A=" << c.A << ",B=" << B << endl; } int Tc::B = 2; void main(void){ Tc a(2), b(4); Tc::display(a); Tc::display(b); }静态成员的继承如果基类定义了一个 static 成员，则在整个继承层次中只存在该成员的唯一定义，不论从基类派生出来多少个派生类，对于每个静态成员来说都只存在唯一的实例。静态成员遵循一般的访问控制规则，如果基类中的成员是 private 的，则派生类无权访问静态成员函数无法被覆盖static 成员函数没有 this 指针，只有非 static 成员函数可以声明为虚函数，虚函数调用的绑定是根据对象类型确定的派生类中定义的同名静态函数会隐藏基类的静态函数，而非覆盖。class Base{ public: static void func() { cout << "Base\n"; } }; class Derived : public Base{ public: static void func() { cout << "Derived\n"; } // 隐藏 Base::func() }; int main(){ Derived::func(); Derived::Base::func(); }5.常对象如果在说明对象时用 const 限定，则被说明的对象为常对象，常对象的数据成员的值在整个生存期内不能被修改。由于常对象的值（包括所有的数据成员的值）不能被改变，因此，通过常对象只能调用类中那些不改变数据成员值的成员函数（即常成员函数），而不能调用类中的其他普通成员函数。class ClassName{}; ClassName const a1; const ClassName a2; // 两种方式都可以创建常对象常成员函数const 说明的成员函数称为常成员函数<typename> func(<args>) const; // const 在最后const 是函数类型的一个组成部分，在函数的实现部分也要使用关键字 const 常成员函数不能修改对象的数据成员，也不能调用该类中没有由关键字 const 修饰的成员函数，从而保证了在常成员函数中不会修改数据成员的值。const 参与区分函数重载如果一个对象被说明为常对象，则通过该对象只能调用它的常成员函数常数据成员类的数据成员中被 const 说明的数据成员称为常数据成员类的数据成员也可以是常量或常引用如果类中说明了常数据成员，构造函数只能通过初始化列表对该数据成员进行初始化，而任何其他函数都不能对该成员赋值。class Person{ private: intage; char *name; public: Person(int, char *); ~Person(); voidPrint(); // 重载函数，用于输出的普通成员函数 voidPrint() const; // 重载函数，用于输出的常成员函数，const参与重载 voidModifyAge(); }; Person::Person(intn, char *na) // 构造函数的定义 { age = n; name = newchar[strlen(na) + 1]; strcpy(name, na); } Person::~Person() // 析构函数的定义 { delete[] name; }6.指向类成员的指针在C++中可以定义一种特殊的指针，它指向类中的成员函数或类中的数据成员。并可通过这样的指针来使用类中的数据成员或调用类中的成员函数。指向类数据成员的指针定义一个指向类中数据成员的指针变量的一般格式为：<typename> ClassName::*PointName;其中 typename 是指针 PointName 所指向数据的类型，它必须是类 ClassName 中某一数据成员的类型1.指向类中数据成员的指针变量不是类中的成员，这种指针变量应在类外定义2.与指向类中数据成员的指针变量同类型的任一数据成员，可将其地址赋给这种指针变量，赋值的一般格式为：PointName = &ClassName::member; mptr = &S::y; //示例：表示将对象S的数据成员y的地址赋给指针变量mptr3.用这种指针访问数据成员时，必须指明是使用哪一个对象的数据成员。当与对象结合使用时，其用法为：ObjectName.* PointName4.由于这种指针变量并不是类的成员，所以使用它只能访问对象的公有数据成员指向类中成员函数的指针变量定义一个指向类中成员函数的指针变量的一般格式为：<typename> (ClassName::*PointName）(<args>);其中 PointName 是指向类中成员函数的指针变量，ClassName 是已定义的类名；typename 是通过函数指针 PointName 调用类中的成员函数时所返回值的数据类型，它必须与类 ClassName 中某一成员函数的返回值的类型相一致，<args> 是函数的形式参数表在使用这种指向成员函数的指针前，应先对其赋值PointName = ClassName::FuncName;1.指向类中成员函数的指针变量不是类中的成员，这种指针变量应在类外定义2.不能将任一成员函数的地址赋给指向成员函数的指针变量，只有成员函数的参数个数、参数类型、参数的顺序和函数的类型均与这种指针变量相同时，才能将成员函数的地址赋给这种指针变量3.使用这种指针变量来调用成员函数时，必须指明调用哪一个对象的成员函数，这种指针变量是不能单独使用，必须用对象名引用4.由于这种指针变量不是类的成员，所以用它只能调用公有的成员函数
第三章 继承和派生类继承性是面向对象程序设计中最重要的机制，这种机制提供了无限重复利用程序资源的一种途径。通过C++语言中的继承机制，可以扩充和完善旧的程序设计以适应新的需求。这样不仅可以节省程序开发的时间和资源，并且为未来程序增添了新的资源。在C++中所谓“继承”就是在一个已存在的类的基础上建立一个新的类已存在的类称为 基类(base class) 或 父类(father class)。新建立的类称为 派生类(derived class) 或 子类(son class) 。1.派生新增内容派生类则除了继承基类的所有可引用成员变量和成员函数外还可另外定义本身的成员变量和处理这些变量的函数语法格式：Access 是继承方式，有 private , public , protected 三种class ClassName:<Access>BaseClassName{ private: ...... ；//私有成员说明 public: ...... ；//公有成员说明 protected: ...... ；//保护成员说明 ｝2.派生方式及访问权限在派生时，原有属性的访问权限在派生类中有所不同。基类成员类型public 继承protected 继承private 继承publicpublicprotectedprivateprotectedprotectedprotectedprivateprivate不可访问不可访问不可访问总结：public 继承不改变基类属性protected 继承将原来的 public 降级为 protected ，其余不变private 继承会将所有的属性变为private ，基类的 private 仍然不可访问派生类属于原有基类外， private 永远不可访问3.抽象类当定义了一个类，这个类只能用作基类来派生出新的类，而不能用这种类来定义对象时，称这种类为抽象类。将类的构造函数或析构函数的访问权限定义为保护时，这种类为抽象类。class A{ int x, y; protected: A(int a, int b){ x = a; y = b; } // 基类初始化 public: void ShowA() { cout << "x=" << x << '\t' << "y=" << y << '\n'; } }; class B : public A{ int m; A a1(1, 1); // 不合法，派生类中不允许创建基类对象（因为属于类外） public: B(int a, int b, int c) : A(a, b) // 可以在派生类中调用A的构造函数 { m = c; } void Show(){ cout << "m=" << m << '\n'; ShowA(); } }; void main(void){ B b1(1, 2, 3); // 可以定义派生类对象 b1.Show(); A aa(2, 3); // 不合法，不允许创建A的对象 }4.多重继承可以用多个基类来派生一个类class ClassName:<Access> Class1, <Access> Class2 { private: ...... ；//私有成员说明; public: ...... ；//公有成员说明; protected: ...... ；//保护的成员说明; }; class D:public A,protected B,private C{ ....//派生类中新增加成员 };5.初始化对象成员构造函数不能被继承，派生类的构造函数必须调用对象所属类的构造函数来初始化对象成员。派生类构造函数的调用顺序如下：1.基类的构造函数2.对象成员的构造函数3.派生类的构造函数当撤销派生类对象时，析构函数的调用正好相反。当调用基类的构造函数时，根据继承参数表中顺序来调用。class Base1{ int x; public: Base1(int a){ x = a; cout << "调用基类1的构造函数 !\n "; } ~Base1() { cout << "调用基类1的析构函数 !\n "; } }; class Base2{ int y; public: Base2(int a){ y = a; cout << "调用基类2的构造函数 !\n "; } ~Base2() { cout << "调用基类2的析构函数 !\n "; } }; class Derived : public Base2, public Base1 { int z; public: Derived(int a, int b) : Base1(a), Base2(20) { z = b; cout << "调用派生类的构造函数 !\n "; } ~Derived() { cout << "调用派生类的析构函数 !\n "; } }; int main(void){ Derived c(100, 200); return 0; // 运行结果如下： // 调用基类2的构造函数 // 调用基类1的构造函数 // 调用派生类的构造函数 // 调用派生类的析构函数 // 调用基类1的析构函数 // 调用基类2的析构函数 }在使用参数表初始化时基类成员构造用基类名，对象成员构造用对象名下方例子中，基类成员 a , b 都使用对应的基类名称 Base1 , Base2 进行初始化对象成员 b1 , b2 使用对象名初始化class Derived : public Base2, public Base1 { int z; Base1 b1, b2; public: Derived(int a, int b) : Base1(a), Base2(20), b1(200), b2(a + b) //a,b是基类成员用基类名称，对象成员用对象名称 { z = b; cout << "调用派生类的构造函数!\n"; } ~Derived() { cout << "调用派生类的析构函数!\n"; } }; void main(void) { Derived c(100, 200); }6.基类与对象成员从基类中继承的成员和继承后新定义的成员会产生各种情况成员名称冲突当派生类中成员的名称和基类中名称冲突时，可以使用类作用符 :: 限定若不加类作用符限制，则优先调用派生类中的成员在下例中，使用 c1.y 调用派生类中的 y，使用 c1.B::y 调用基类中的 yclass A{ public: int x; void Show() { cout << "x=" << x << '\n'; } }; class B{ public: int y; void Show() { cout << "y=" << y << '\n'; } }; class C : public A, public B{ public: int y; // 类B和类C均有y的成员 }; void main(void){ C c1; c1.x = 100; c1.y = 200; // 给派生类中的y赋值 c1.B::y = 300; // 给基类B中的y赋值 c1.A::Show(); c1.B::Show(); // 用作用域运算符限定调用的函数 cout << "y=" << c1.y << '\n'; // 输出派生类中的y值 cout << "y=" << c1.B::y << '\n'; // 输出基类B中的y值 }基类派生次数任一基类在派生类中只能继承一次，否则会造成成员名的冲突若在派生类中，确实要有二个以上基类的成员，则可用基类的二个对象作为派生类的成员。把一个类作为派生类的基类或把一个类的对象作为一个类的成员，在使用上是有区别的：在派生类中可直接使用基类的成员（访问权限允许的话），但要使用对象成员的成员时，必须在对象名后加上成员运算符“.”和成员名。class Dot{ public: float x, y; Dot(float a = 0, float b = 0){ x = a; y = b; } void Show(void) { cout << "x=" << x << '\t' << "y=" << y << endl; } }; class Line : public Dot{ Dot d1, d2; // 在 Line 类中定义两个基类Dot的对象 public: Line(Dot dot1, Dot dot2) : d1(dot1), d2(dot2) // 使用两个基类的对象作为初始化参数 { x = (d1.x + d2.x) / 2; y = (d1.y + d2.y) / 2; } void Showl(void){ cout << "Dot1: "; d1.Show(); cout << "Dot2: "; d2.Show(); cout << "Length=" << sqrt((d1.x - d2.x) * (d1.x - d2.x) + (d1.y - d2.y) * (d1.y - d2.y)) << endl; cout << "Center: " << "x=" << x << '\t' << "y=" << y << endl; } }; void main(void){ float a, b; cout << "Input Dot1: \n"; cin >> a >> b; Dot dot1(a, b); // 调用Dot的构造函数 cout << "Input Dot2: \n"; cin >> a >> b; Dot dot2(a, b); Line line(dot1, dot2); line.Showl(); }7.对象赋值规则可以将派生类对象的值赋给基类对象，反之不行只是给从基类继承来的成员赋值Dot dot; Line line; dot = line; line = dot; // 不合法指针赋值规则可以将一个派生类对象的地址赋给基类的指针变量此时的指针只能指向从基类继承来的数据Base *basep; // 基类指针 basep = &b; // 指向基类对象 basep = &d; // 指向派生类对象引用初始化规则派生类对象可以初始化基类的引用Derive d; Base &basei=d; // 允许使用派生类的对象作为基类的引用8.派生类向基类的类型转换赋值兼容规则（向上类型转换）在任何需要基类对象（或地址）的地方，都可以由其公有派生类的对象（或地址）代替。公有派生类就是基类的子类型，公有派生类的对象可以自动转换为基类类型，基类的指针和引用可以指向派生类的对象存在继承关系的类型之间的转换规则从派生类向基类的类型转换只对指针或引用类型有效，对象转换存在切片现象私有继承和保护继承不能应用派生类到基类的类型转换对象向上类型转换和对象切片派生类对象在向基类对象转换时，会发生“对象切片”现象——派生类对象被“切片”，直到剩下适合的基类子对象对象切片实际上是在将它复制到一个新对象时，去掉原来对象的一部分，因此，不建议使用指针和引用向上类型转换使用指针向上类型转换只是简单地改变地址的类型，是用不同的方式解读同一段内存空间中的内容，不会真正切除派生类对象的多余部分引用向上类型转换与指针相似，指针和引用在进行转换时不会发生对象切片的现象9.虚基类如果同一个公共的基类在派生类中产生多个拷贝，不仅多占用了存储空间，而且可能会造成多个拷贝中的数据不一致和模糊的引用。 在多重派生的过程中，若使公共基类在派生类中只有一个拷贝，则可将这种基类说明为虚基类虚基类在基类的类名前加上关键字 virtualclass B:public virtual A{ public: int y; B(int a = 0, int b = 0):A(b) { y=a;} };虚基类构造函数如果虚基类没有缺省的构造函数，则在每一个派生类的构造函数中都必须有对虚基类构造函数的调用 （且首先调用）。一般在列表参数中直接显式调用虚基类构造函数class A{ public: int x; A(int a = 0) { x = a; } }; class B : public virtual A{ public: int y; B(int a = 0, int b = 0) : A(a) { y = b; } }; class C : public virtual A{ public: int z; C(int a = 0, int c = 0) : A(a) { z = c; } }; class D : public B, public C{ public: int dx; D(int a1, int b, int c, int d, int a2) : B(a1, b), C(a2, c), A(a2) // 在派生类中调用虚基类构造函数 { dx = d; } };10.覆盖与隐藏在派生类中重定义基类接口中的成员函数，会发生覆盖或者隐藏两种情况形式参数表与返回类型能否处理基类信息覆盖(override)保持与基类中一致可以隐藏(name hiding)改变了函数的参数表或返回类型不可以class B{ public: void f(int) {; } void f() {; } void g(char) { ; } void h() {; } }; class D1 : public B{ public: void h() {; } // 重定义，覆盖了B中的h() }; class D2 : public B{ public: void f() {; } // 覆盖了B中的f()，同时隐藏了f(int) void g() {; } // 隐藏了B中的g(char) };类作用域和名字隐藏C++中每个类都定义了一个类作用域，派生类的作用域是包含在其基类作用域之内的作用域的同名隐藏原则：内层作用域中的名字将隐藏外围作用域中相同的名字作用域中名字的查找规则：由内层向外层逐步查找，找到即隐藏在下例子中，作用域是 D2 < D1 < B 的因此 D2 优先会覆盖 D1 中的函数 f()class B { public: virtual void f() { cout << "B::f" << endl; } }; class D1 : public B { public: void f() override { cout << "D1::f" << endl; } // 覆盖 B::f }; class D2 : public D1 { public: void f() override { cout << "D2::f" << endl; } // 覆盖 D1::f };
OOP课程笔记：第二章 构造函数与析构函数
第二章 构造函数与析构函数构造函数是在创建对象时，使用给定的值来将对象初始化。析构函数是在系统释放对象前，对对象做一些善后工作。构造函数是类的成员函数，系统约定构造函数名必须与类名相同。构造函数可以带参数、可以重载，同时没有返回值class A{ float x, y; public: A(float a, float b){ x = a; y = b; } //构造函数，初始化对象 float Sum(void) { return x + y; } void Set(float a, float b){ x = a; y = b; } Print(void) { cout << "x=" << x << '\t' << "y=" << y << endl; } }; void main(void){ A a1(2.0, 3.0); // 定义时调用构造函数初始化 A a2(1.0, 2.0); a2.Set(10.0, 20.0); // 利用成员函数重新为对象赋值 a1.Print(); a2.Print(); }注意点:1.构造函数的函数名必须与类名相同。2.在定义构造函数时，不能指定函数返回值的类型，也不能指定为 void 类型。3.一个类可以定义若干个构造函数。当定义多个构造函数时，必须满足函数重载的原则。4.构造函数可以指定参数的缺省值。5.若定义的类要说明该类的对象时，构造函数必须是公有的成员函数。如果定义的类仅用于派生其它类时，则可将构造函数定义为保护的成员函数。6.由于构造函数属于类的成员函数，它对私有数据成员、保护的数据成员和公有的数据成员均能进行初始化1.缺省构造函数若没有显式定义构造函数，系统默认缺省的构造函数。缺省的构造函数并不对所产生对象的数据成员赋初值，即新产生对象的数据成员的值是*不确定的。class A{ float x, y; public: A() {} // 隐含的缺省的构造函数 void Print(void) { cout << x << '\t' << y << endl; } }; A a1, a2; 2.对局部对象，静态对象，全局对象的初始化对于局部对象，每次定义对象时，都要调用构造函数。对于静态对象，是在首次定义对象时，调用构造函数的，且由于对象一直存在，只调用一次构造函数。对于全局对象，是在 main() 执行之前调用构造函数的。3.参数初始化列表通过参数初始化列表来实现对数据成员的初始化，在构造函数首部实现。Box∷Box(int h,intw,intlen):height(h),width(w),length(len){}注意：如果数据成员是数组，则应当在构造函数的函数体中用语句对其赋值class Student{ public: Student(int n, charchar s, nam[]) : num(n), sex(s) // 定义构造函数 { strcpy(name, mam); // 数组在函数体完成赋值 } // 函数体 private: int num; char sex; char name[20]; };1.在定义类时，只要显式定义了一个类的构造函数，则编译器就不产生缺省的构造函数2.所有的对象在定义时，必须调用构造函数，不存在没有构造函数的对象class A{ float x, y; public: A(float a, float b){ x = a; y = b; } void Print(void) { cout << x << '\t' << y << endl; } }; void main(void){ A a1; // 会发生错误，没有构造函数调用 A a2(3.0, 30.0); }4.构造函数与 new 运算符可以使用 new 运算符来动态地建立对象。建立时要自动调用构造函数，以便完成初始化对象的数据成员。最后返回这个动态对象的起始地址。用 new 运算符产生的动态对象，在不再使用这种对象时，必须用 delete 运算符来释放对象所占用的存储空间。用 new 建立类的对象时，可以使用参数初始化动态空间。class A{ float x, y; public: A(float a, float b){ x = a; y = b; } A(){ x = 0; y = 0; } void Print(void) { cout << x << '\t' << y << endl; } }; void main(void){ A *pa1, *pa2; pa1 = new A(3.0, 5.0); // 用new动态开辟对象空间，初始化 pa2 = new A; // 用new动态开辟空间，调用构造函数初始化 pa1->Print(); pa2->Print(); delete pa1; // 用delete释放空间 delete pa2; // 用delete释放空间 }5.析构函数1.析构函数是成员函数，函数体可写在类体内，也可写在类体外2.析构函数是一个特殊的成员函数，函数名必须与类名相同，并在其前面加上字符 ~3.析构函数不能带有任何参数，不能有返回值，不指定函数类型4.一个类中，只能定义一个析构函数，析构函数不允许重载。5、析构函数是在撤消对象时由系统自动调用的。在程序的执行过程中，当遇到某一对象的生存期结束时，系统自动调用析构函数，然后再收回为对象分配的存储空间。class A { float x, y; public: A(float a, float b){ x = a; y = b; cout << "调用非缺省的构造函数\n "; } A(){ x = 0; y = 0; cout << "调用缺省的构造函数\n "; } ~A() { cout << "调用析构函数\n "; } void Print(void){ cout << x << '\t' << y << endl; } }; void main(void){ A a1; A a2(3.0, 30.0); cout << "退出主函数\n "; } //运行结果： //调用缺省的构造函数 //调用非缺省的构造函数 //退出主函数 //调用析构函数 //调用析构函数new 对象的析构在程序的执行过程中，对象如果用 new 运算符开辟了空间，则在类中应该*定义一个析构函数，并在析构函数中使用 delete 删除由 new 分配的内存空间。因为在撤消对象时，系统自动收回为对象所分配的存储空间，而不能自动收回由 new 分配的动态存储空间。当使用运算符 delete 删除一个由 new 动态产生的对象时，它首先调用该对象的析构函数，然后再释放这个对象占用的内存空间。class Str{ char *Sp; int Length; public: Str(char *string){ if (string){ Length = strlen(string); Sp = new char[Length + 1]; //在构造函数中将成员数据指针指向动态开辟的内存 strcpy(Sp, string); // 用初值为开辟的内存赋值 } else Sp = 0; } void Show(void) { cout << Sp << endl; } ~Str(){ if (Sp) delete[] Sp; // 析构函数，当释放对象时收回用new开辟的空间 } }; void main(void){ Str s1("Study C++"); s1.Show(); }6.不同类型的对象调用构造和析构函数对象类型构造函数析构函数全局定义对象程序开始运行程序结束局部定义对象程序执行到定义对象处退出对象作用域static定义局部对象首次到达对象定义处程序结束new 运算符动态生成对象产生对象时delete 时，否则程序结束仍然存在动态构造及析构对象数组用 new 运算符来动态生成对象数组时，自动调用构造函数，而用 delete 运算符来释放指针所指向的对象数组占用的存储空间时，在指针变量的前面必须加上 [ ]，才能将数组元素所占用的空间全部释放。否则，只释放第0个元素所占用的空间。class A{ float x, y; public: A(float a = 0, float b = 0){ x = a; y = b; cout << "调用了构造函数\n"; } void Print(void) { cout << x << '\t' << y << endl; } ~A() { cout << "调用了析构函数\n"; } }; void main(void){ cout << "进入main()函数\n"; A *pa1; pa1 = new A[3]; // 开辟数组空间 cout << "\n完成开辟数组空间\n\n"; delete[] pa1; // 必须用[]删除开辟的空间 cout << "退出main()函数\n"; }7.缺省的析构函数若在类的定义中没有显式地定义析构函数时，则编译器自动地产生一个缺省的析构函数，其格式为ClassName::~ClassName(){};任何对象都必须有构造函数和析构函数但在撤消对象时，要释放对象的数据成员用 new 运算符分配的动态空间时，必须显式地定义析构函数。8.完成拷贝功能的构造函数可以在定义一个对象的时候用另一个对象为其初始化，即构造函数的参数是另一个对象的引用，这种构造函数常为完成拷贝功能的构造函数class A{ float x, y; public; A(float a = 0, float b = 0){ x = a; y = b; } A(A &a){ // 引用作为形参 x = a.x; // 完成初始化功能 y = a.y; } }; void main(void){ A a1(1.0, 2.0); A a2(a1); }缺省的拷贝构造函数如果没有定义完成拷贝功能的构造函数，编译器自动生成一个隐含的完成拷贝功能的构造函数，依次完成类中对应数据成员的拷贝。A::A(A &a){ x=a.x; y=a.y; }new的拷贝构造函数当类中的数据成员中使用 new 运算符，动态地申请存储空间进行赋初值时，必须在类中显式地定义一个完成拷贝功能的构造函数，以便正确实现数据成员的拷贝class Str{ int Length; char *Sp; public: Str(char *string){ if (string){ Length = strlen(string); Sp = new char[Length + 1]; strcpy(Sp, string); } else Sp = 0; } void Show(void) { cout << Sp << endl; } ~Str(){ if (Sp) delete[] Sp; // delete 内存地址 } }; //隐含的拷贝构造函数为： Str::Str(Str &s){ Length=s.Length; Sp=s.Sp; // 新的指针指向的数据是同一个地址 } void main(void) { Str s1("Study C++"); Str s2(s1); s1.Show(); s2.Show(); // 同一个地址被delete两次会发生错误 }在这种情况下，必须要定义完成拷贝功能的构造函数。Str::Str(Str &s){ if (s.Sp){ Length = s.Length; Sp = new char[Length + 1]; // 额外开辟一个新的空间，并非地址赋值 strcpy(Sp, s.Sp); } else Sp = 0; }9.构造函数与对象成员在类 A 中 包含类 B 的对象对类 A 的对象初始化的同时还要对其成员数据类 B 的对象进行初始化，所以，类 A 的构造函数中要调用类 B 的构造函数class B{ ...// 省略 }; class A{ ...//省略 B b1, b2; };使用构造函数来对成员对象进行初始化a1(b, c) 调用 A 的构造函数进行初始化class A{ float x, y; public: A(int a, int b){ x = a; y = b; } void Show() { cout << "x=" << x << '\t' << "y=" << y << '\n'; } }; class C{ float z; A a1; // 类C的数据成员为类A的对象a1 public: C(int a, int b, int c) : a1(b, c) { z = a; } // 类C的对象初始化 void Show(){ cout << "z=" << a << '\n'; a1.Show(); } }; void main(void){ C c1(1, 2, 3); // 对类C的对象初始化 c1.Show(); }对象成员的构造函数顺序对对象成员的构造函数的调用顺序取决于这些对象成员在类中说明的顺序，与它们在成员初始化列表中的顺序无关。析构函数的调用顺序与构造函数正好相反。
第一章 类和对象1.基本概念对象是由一组属性(attribute)和一组行为(behavior)构成的。在C++中，每个对象都是由数据和函数(即操作代码)这两部分组成的。面向对象程序设计中每一组数据都是有特定的用途的，是某种操作的对象。也就是说，一组操作调用一组数据。把相关的数据和操作放在一起，形成一个整体，与外界相对分隔。这就是面向对象的程序设计中的对象。在C++，中对象的类型称为类(class)。类代表了某一批对象的共性和特征。类是对象的抽象，而对象是类的具体实例(instance)。class Student{ private : char Name[20]; float Math; float Chinese; public : float average; void SetName(char *name); void SetMath(float math); void SetChinese(float ch); float GetAverage(void); }; // 分号注意不要少2.关键字关键字成员类型作用域private私有成员只允许该类内的成员函数使用私有的成员数据public公有成员可以在类内或类外自由使用protected保护成员仅允许该类及该类的派生类3.类的成员如果未加说明，类中成员默认的访问权限是private，即私有的。在下示例中，在类外不能直接使用 x 或 y ，必须通过 Setxy()给 x 或 y 赋值，通过 Print() 输出 x 或 y 。class A{ float x, y; public: void Setxy(float a, float b){ x = a; y = b; } void Print(void){ cout << x << ‘\t’ << y << endl;} };成员函数与成员数据的定义不分先后，可以先说明函数原型，再在类体外定义函数体。class A{ float x, y; public: void Setxy(float a, float b); // 体内声明函数 void Print(void); }; void A::Setxy(float a, float b){ // 体外定义 x = a; y = b; } void A::Print(void){ cout << x << '\t' << y << endl; }注意两点 :1.类具有封装性，并且类只是定义了一种结构，所以类中的任何成员数据均不能使用关键字 extern ， auto 或 register 限定其存储类型。2.在定义类时，只是定义了一种导出的数据类型，并不为类分配存储空间，所以，在定义类中的数据成员时，不能对其初始化。class Test { int x = 5,y = 6; // 是不允许的 extern float x; // 是不允许的 };struct 与 class在C++语言中，结构体类型 struct 只是类 class 的一个特例。结构体类型与类的唯一的区别在于：在类中，其成员的缺省的存取权限是私有的；而在结构体类型中，其成员的缺省的存取权限是公有的。内联成员函数当我们定义一个类时，可以在类中直接定义函数体。这时成员函数在编译时是作为内联函数来实现的。同时，我们也可以在类体外定义类的内联成员函数，在类体内声明函数在类体外定义时，在成员函数的定义前面加上关键字 inline 。class A{ float x, y; public: void Setxy(float a, float b); // 声明内联函数 void Print(void); }; inline void A::Setxy(float a, float b){ // 定义内联函数 x = a; y = b; } inline void A::Print(void){ cout << x << 't' << y << endl;}4.对象在定义类时，只是定义了一种数据类型，即说明程序中可能会出现该类型的数据，并不为类分配存储空间。类的变量我们称之为对象，对象是类的实例。在建立对象时，只为对象分配用于保存数据成员的内存空间，而成员函数的代码为该类的每一个对象所共享。对象的使用一个对象的成员就是该对象的类所定义的成员，有成员数据和成员函数，引用时同结构体变量类似，用 . 运算符。class A{ float x, y; public: float m, n; void Setxy(float a, float b){ x = a; y = b; } void Print(void){ cout << x << '\t' << y << endl; } }; void main(void){ A a1, a2; // 定义对象 a1.m = 10; a1.n = 20; // 为公有成员数据赋值 a1.Setxy(2.0, 5.0); // 为私有成员数据赋值 a1.Print(); }访问私有成员用成员选择运算符 . 只能访问对象的公有成员，而不能访问对象的私有成员或保护成员。若要访问对象的私有的数据成员，只能通过对象的公有成员函数来获取。class A{ float x, y; public: float m, n; void Setxy(float a, float b){ x = a; y = b; } void Print(void){ cout << x << '\t' << y << endl; } }; void main(void) { A a1, a2; a1.m = 10; a1.n = 20; // 为公有成员数据赋值 a1.x = 2; // 非法，私有成员不可以在类外访问 a1.y = 5; // 非法，私有成员不可以在类外访问 a1.Setxy(2.0, 5.0); a1.Print(); }同类型的对象之间可以整体赋值，这种赋值与对象的成员的访问权限无关。class A{ float x, y; public: float m, n; void Setxy(float a, float b){ x = a; y = b; } void Print(void){ cout << x << '\t' << y << endl; } }; void main(void){ A a1, a2; a1.m = 10; a1.n = 20; a1.Setxy(2.0, 5.0); a2 = a1; // 同类型对象赋值，相当于成员数据之间相互赋值 a1.Print(); a2.Print(); }类的作用域类体的区域称为类作用域。类的成员函数与成员数据，其作用域都是属于类的作用域，仅在该类的范围内有效。class A{ float x, y; public: float m, n; void Setxy(float a, float b){ x = a; y = b; } void Print(void) { cout << x << '\t' << y << endl;} }; void main(void){ A a1, a2; a1.m = 20; a1.n = 10; a1.Setxy(2.0, 5.0); // 类的作用域内 a1.Print(); // 类的作用域内 Setxy(2.0, 5.0); // 不允许 Print(); // 不允许 }对象的作用域分为全局对象、局部对象、局部静态对象等。class A // 文件作用域，整个文件有效 { float x, y; public: float m, n; void Setxy(float a, float b){ x = a; y = b; } void Print(void){ cout << x << '\t' << y << endl; } } a3, a4; void main(void){ A a1, a2; class B{ // 块作用域，在函数内有效 int i, j; public: void Setij(int m, int n){ i = m; j = n; } }; B b1, b2; a1.Setxy(2.0, 5.0); b1.Setij(1, 2); }类的嵌套类B包含在类A中，为嵌套定义。在类 A 的定义中，并不为 b1 , b2 分配空间，只有在定义类 A 的对象时，才为嵌套类的对象分配空间。嵌套类的作用域在类 A 的定义结束时结束。class A{ class B{ int i, j; public: void Setij(int m, int n){ i = m; j = n; } }; float x, y; public: B b1, b2; // 嵌套类的对象 void Setxy(float a, float b){ x = a; y = b; } void Print(void){ cout << x << '\t' << y << endl; } };5.成员函数成员函数的重载成员函数可以带有缺省的参数，也可以重载成员函数。重载时，函数的形参必须在类型或数目上不同。class Test{ int x, y; int m, n; public: void Setxy(int a, int b){ x = a; y = b; } void Setxy(int a, int b, int c, int d){ x = a; y = b; m = c; n = d; } void Printxy(int x) { cout << “m = "<<m<<'\t'<<“n=" << n << endl; } void Printxy(void) { cout << "x=" << x << '\t' << "y=" << y << endl; } }; void main(void) { Test p1, p2; p1.Setxy(3, 5); p2.Setxy(10, 20, 30, 40); // 参数不同 p1.Printxy(); p2.Printxy(); p2.Printxy(2); // 参数类型不同 }使用指针引用对象成员函数class A{ float x, y; public: float Sum(void) { return x + y; } void Set(float a, float b){ x = a; y = b; } void Print(void) { cout << "x=" << x << '\t' << "y=" << y << endl; } }; void main(void) { A a1, a2; A *p; // 定义类的指针 p = &a1; // 给指针赋值 p->Set(2.0, 3.0); // 通过指针引用对象的成员函数 p->Print(); cout << p->Sum() << endl; a2.Set(10.0, 20.0); a2.Print(); }6.this指针不同对象占据内存中的不同区域，它们所保存的数据各不相同，但对成员数据进行操作的成员函数的程序代码均是一样的。当对一个对象调用成员函数时，编译程序先将对象的地址赋给 this 指针，然后调用成员函数，每次成员函数存取数据成员时，也隐含使用this指针。this 指针具有如下形式的缺省说明，实际上是一个常量指针class classname{}; classname *const this; // this实际上是一个常量指针以一个字符串的类作为示例代码class S{ public: char *strp; int length; void Ini(char *s); void Print(void); void Copy(S &s); }; void S::Copy(S &s){ if (&s == this) // this 指针和传入的待复制的类进行比较 cout << "不能复制本身\n "; else{ if (strp) delete[] strp; length = s.length; strp = new char[length + 1]; strcpy(strp, s.strp); } }