C++ 实验笔记 Lab1–Lab10

0 总览

  • Lab1:随机数、格式化输出、文件读写、getline
  • Lab2:三种参数传递(值 / 指针 / 引用),以及格式化输出
  • Lab3 & Lab10:模板类实现细节(template<class T>、友元、显式实例化)
  • Lab4assign(const String& other)const 与引用
  • Lab5:构造/拷贝/赋值、static 成员初始化、const 成员初始化列表
  • Lab6:继承:基类构造、初始化列表、调用基类成员函数
  • Lab7:带默认参数的构造函数、NULL/空串处理、友元运算符
  • Lab8operator=、资源管理、(简化版)引用计数智能指针思路
  • Lab9:虚函数 vs 纯虚函数(含 const 签名一致性)

1 Lab1:随机数、格式化输出与文件读写

1.1 随机数:srandrand

常见用法:用当前时间作为随机种子(seed),避免每次运行得到相同序列。

1
2
3
4
5
#include <cstdlib>
#include <ctime>

srand(time(0)); // 以当前时间作为种子
int a = rand(); // 生成伪随机整数

1.2 输出格式控制:<iomanip>

  • setprecision(2):设置浮点输出精度(常配合 fixed
  • setw(2):设置最小输出宽度
  • setfill('0'):宽度不足时用指定字符补齐
1
2
3
4
5
6
#include <iostream>
#include <iomanip>
using namespace std;

cout << fixed << setprecision(2) << 3.14159 << endl; // 3.14
cout << setw(2) << setfill('0') << 7 << endl; // 07

1.3 文件输入输出:ifstream / ofstream

输入文件流(读)ifstream
输出文件流(写)ofstream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <fstream>
#include <iostream>
#include <string>
using namespace std;

int main() {
string filename = "../Pro2_rand/rand.cpp"; // ../ 表示上一级目录(相对路径)
ifstream inFile(filename);
if (!inFile.is_open()) {
cerr << "无法打开文件: " << filename << endl;
return 1; // 非 0 通常表示异常退出
}

ofstream outFile("copy.cpp"); // 在当前运行目录生成/覆盖
if (!outFile.is_open()) {
cerr << "无法创建文件: copy.cpp" << endl;
return 1;
}

// ...
}

注意:ofstream("copy.cpp") 默认覆盖写,若文件已存在会清空后重写。

1.4 getline 逐行读取

getline(inFile, line):从 inFile 读一行(不包含换行符)到 line
while(getline(...)):读成功继续;读到文件末尾或出错停止。

1
2
3
4
string line;
while (getline(inFile, line)) {
cout << line << endl; // 原样输出到屏幕
}

2 Lab2:三种参数传递与 swap

2.1 值传递 / 指针传递 / 引用传递

对比三种交换函数(核心是:能否修改实参):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1) 值传递:不会影响外部变量(只改了副本)
void swap1(int a, int b) {
int t = a; a = b; b = t;
}

// 2) 指针传递:通过解引用修改实参
void swap2(int* a, int* b) {
int t = *a; *a = *b; *b = t;
}

// 3) 引用传递:语法最干净,本质也是改实参
void swap3(int& a, int& b) {
int t = a; a = b; b = t;
}

2.2 时间/数字格式化输出(补 0)

1
2
3
#include <iomanip>
// ...
cout << setw(2) << setfill('0') << hour << endl;

3 Lab3 & Lab10:模板类实现要点(Array

重点:模板类/模板函数的定义通常要让编译器“看得到”,因此常放在头文件;如果拆到 .cpp,就要做显式实例化

3.1 在 .cpp 写模板成员:每个定义都要带 template<class T>

1
2
3
4
5
6
7
8
template<class T>
Array<T>::Array(int size) : size(size) {
if (size > 0) arr = new T[size];
else {
this->size = 0;
arr = nullptr;
}
}

3.2 友元运算符声明也要是模板

关键点:友元声明前也要加模板参数。

1
2
3
4
5
6
template<class T>
class Array {
// ...
template<class U>
friend std::ostream& operator<<(std::ostream& out, const Array<U>& rhs);
};

3.3 类型转换运算符:operator T*()

当你想把“动态数组对象”转成原生指针时,可写转换运算符(但要谨慎使用,避免隐式转换带来歧义)。

1
2
3
4
5
6
7
8
template<class T>
class Array {
public:
operator T*() { return arr; } // 转成 T*
operator const T*() const { return arr; }
private:
T* arr = nullptr;
};

3.4 operator+:先算新大小,再构造结果数组

1
2
3
4
5
6
7
template<class T>
Array<T> Array<T>::operator+(const Array& rhs) const {
int new_size = this->size + rhs.size;
Array<T> result(new_size); // 用构造函数直接创建新数组
// ... 把两边元素拷贝进 result
return result;
}

3.5 显式实例化(写在 .cpp 末尾)

当模板定义放在 .cpp,你必须告诉编译器“我要生成哪些类型”。

1
2
3
4
5
template class Array<int>;
template class Array<int*>;

// 如果你把 operator<< 定义也放在 .cpp,通常也需要显式实例化对应版本
template std::ostream& operator<<(std::ostream& out, const Array<int>& rhs);

4 Lab4:assign(const String& other) 的 const 与引用

核心就是:用引用避免拷贝,用 const 保证不修改参数

1
void assign(const String& other);

5 Lab5:构造/拷贝/赋值、static 与 const 初始化

5.1 三个常见构造函数

1
2
3
4
5
6
class String {
public:
String(); // 默认构造
String(const char* s); // 普通构造
String(const String& other); // 拷贝构造
};

5.2 static 成员初始化要在类外

1
2
3
4
5
6
class MyClass {
public:
static int number;
};

int MyClass::number = 0; // 必须类外定义 + 初始化

5.3 const 成员必须用初始化列表

1
2
3
4
5
6
class MyClass {
public:
MyClass() : pi(3.14) {}
private:
const double pi;
};

operator= 是对已有对象赋新值,不会走构造;如果你用 static 计数对象数量,要分清 构造/析构 才是对象生命周期变化点。


6 Lab6:继承与初始化列表(先构造基类,再构造派生类)

6.1 派生类构造函数:用 : 调基类构造

1
2
3
4
5
6
7
8
9
10
11
12
class cemployee : public cperson {
public:
cemployee(const string& name, const string& id, int sex, int age,
const string& department, float salary);
private:
string department;
float salary;
};

cemployee::cemployee(const string& name, const string& id, int sex, int age,
const string& department, float salary)
: cperson(name, id, sex, age), department(department), salary(salary) {}

6.2 调用基类成员函数:Base::func()

1
2
3
4
void cemployee::print() const {
cperson::print(); // 先打印基类部分
cout << department << " " << salary << endl;
}

7 Lab7:默认参数构造与友元运算符

7.1 MyString(const char* str = NULL):NULL 代表“空指针”,不是“空串”

典型写法:如果传入 NULL,就构造一个空字符串 ""

1
2
3
4
5
6
7
8
9
MyString::MyString(const char* str) {
if (str == NULL) {
m_data = new char[1];
m_data[0] = '\0';
} else {
m_data = new char[strlen(str) + 1];
strcpy(m_data, str);
}
}

7.2 “基本类型 + 类” 运算:用友元更方便

当出现 double + complexd 这种左操作数不是类对象的情况,成员函数做不到(因为左边不是 this),通常用友元/非成员函数:

1
friend complexd operator+(double h, const complexd& rhs);

8 Lab8:operator= 与资源管理(含引用计数思路)

8.1 先说清楚三种语义

关键点:把指针直接转移走并把右边置空,更像移动赋值。但如果签名写成 share& operator=(share&),语义会混乱。更稳妥的方向是:

  • 深拷贝:两个对象各自拥有独立资源
  • 共享资源:引用计数(见下一节)
  • 移动语义:应使用 share& operator=(share&&)(C++11 以后)

8.2 引用计数(简化版 shared_ptr 思路)

下面是一个能覆盖你笔记核心点的“引用计数版本”框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class share {
public:
share() : point(nullptr), count(new int(1)) {}
explicit share(int* p) : point(p), count(new int(1)) {}

// 拷贝构造:共享同一资源,计数 +1
share(const share& other) : point(other.point), count(other.count) {
++(*count);
}

// 拷贝赋值:先释放自己,再接管对方的共享关系
share& operator=(const share& other) {
if (this == &other) return *this;

release();

point = other.point;
count = other.count;
++(*count);
return *this;
}

~share() { release(); }

private:
void release() {
if (count && --(*count) == 0) {
delete point;
delete count;
point = nullptr;
count = nullptr;
}
}

int* point;
int* count; // 引用计数
};

9 Lab9:虚函数与纯虚函数

9.1 纯虚函数:派生类必须实现

1
2
3
4
class Base {
public:
virtual void print() const = 0; // 纯虚函数
};
  • 如果基类里写了 virtual ... const = 0
    那么派生类实现时签名必须一致(包括 const)。

9.2 普通虚函数:可以选择重写

普通 virtual 不强制派生类实现;不重写就用基类版本。


10 小结

高频点主要就三类:

  1. 资源管理:构造/拷贝/赋值/析构要配套(避免泄漏、重复释放)
  2. 继承多态:虚函数签名一致、初始化列表构造基类、需要时调用 Base::func()
  3. 模板编译模型:模板定义可见性(头文件 or 显式实例化)