C++期末救命讲义 按样题考点整理
💻 第3、38、39、40、42、43题

04 编程题模板:题干先拆,代码逐行背

这页不是只给答案。每题都先放题干,再告诉你为什么这样写,最后标出题目改了要改哪里。

第3题

输入整数,判断是否合法,然后求平方

从键盘输入一个整数,求它的平方。要求做容错处理,判断输入的是否为数字。
关键词:“输入整数” → 用 int n;cin
关键词:“判断是否为数字” → 用 if (cin >> n) 判断输入是否成功。
关键词:“平方” → 输出 n * n
#include <iostream>          // 使用 cin 和 cout 必须包含 iostream
using namespace std;         // 这样可以直接写 cin、cout,不用 std::cin

int main() {
    int n;                   // n 用来保存用户输入的整数

    cout << "请输入一个整数: ";

    // cin >> n 如果成功,说明输入能被当成整数
    // 比如输入 5 成功;输入 abc 失败
    if (cin >> n) {
        cout << n * n << endl;   // 平方就是 n 乘 n
    } else {
        cout << "输入错误,不是数字" << endl;
    }

    return 0;                 // main 正常结束
}
如果题目改了改哪里
改成求立方n * n 改成 n * n * n
改成输入小数int n 改成 double n
改成输出提示语不同只改 cout 里的文字,不动判断逻辑。
第38题

数组最大值模板函数

编写求数组最大值的模板函数,使如下程序输出 20 3
int arr1[] = {10, 20, 15, 12};
char arr2[] = {1, 2, 3};
cout << arrMax<int>(arr1, n1) << endl;
cout << arrMax<char>(arr2, n2);
template <typename T>:T 是类型占位符,int、char、double 都能塞进来。
T arr[]:数组里装的是 T 类型。
T maxVal:最大值也应该是 T 类型,数组是什么类型,最大值就是什么类型。
// template 表示这是模板函数
// typename T 表示 T 是一个“类型占位符”
template <typename T>
T arrMax(T arr[], int n) {
    T maxVal = arr[0];        // 先假设第 0 个元素最大

    // 从第 1 个元素开始比较,因为第 0 个已经拿来当初始最大值了
    for (int i = 1; i < n; i++) {
        if (arr[i] > maxVal) {
            maxVal = arr[i];  // 发现更大的,就更新最大值
        }
    }

    return maxVal;            // 把最大值交回给 main
}
如果题目改了改哪里
改成求最小值arr[i] > maxVal 改成 arr[i] < minVal,变量名也可以改成 minVal。
数组元素变成 double函数不用改,调用时写 arrMax<double>
输出不同行改 main 里的 cout,模板函数不动。
第39题

文件写入和读取

运行题目给出的写文件程序后,文件 example.txt 中的内容是什么?再编写程序读取 example.txt 的内容并显示。

题目给的写文件程序:它先把内容写进文件

#include <iostream>
#include <fstream>              // 文件流:ofstream / ifstream 都在这里
using namespace std;

int main() {
    // ofstream = output file stream,专门负责“往文件里写”
    ofstream myfile("example.txt");

    if (myfile.is_open()) {      // 文件成功打开,才可以写
        myfile << "This is a line.\n";
        myfile << "This is another line.\n";
        myfile.close();          // 写完关闭文件
    } else {
        cout << "Unable to open file";
    }

    return 0;
}
文件内容
This is a line.
This is another line.
ofstream:out file stream,写文件。
ifstream:in file stream,读文件。
getline(myfile, line):从文件读一整行到 line。

你要补写的读文件程序:再从文件里读出来

#include <iostream>          // cout
#include <fstream>           // ifstream / ofstream
#include <string>            // string 和 getline
using namespace std;

int main() {
    ifstream myfile("example.txt"); // 打开 example.txt 准备读取
    string line;                     // line 用来保存每一行文字

    if (myfile.is_open()) {          // 判断文件是否成功打开
        while (getline(myfile, line)) {
            cout << line << endl;   // 读到一行,就输出一行
        }
        myfile.close();              // 用完文件要关闭
    } else {
        cout << "Unable to open file";
    }

    return 0;
}
如果题目改了改哪里
文件名改了"example.txt"
要求写文件ifstream 换成 ofstream,用 myfile << ... 写入。
要求逐词读取getline(myfile, line) 改成 myfile >> word
第40题

继承 + virtual print

定义 computer 类,包含 nameprint;定义派生类 Macintosh,包含 colorprint。测试代码用 computer *p 指向 Macintosh 对象,并希望输出 name 和 color。
int main() {
    computer *p;
    Macintosh imac("Joe's IMAC", "Blue");
    p = &imac;
    p->print();
    return 0;
}
#include <iostream>
#include <string>
using namespace std;

class computer {
protected:
    string name;                  // protected: 子类可以直接用 name
public:
    computer(string n) : name(n) {} // 构造函数:把参数 n 存进 name

    // 关键:父类指针 p 调用 print 时,想进入子类版本,就必须 virtual
    virtual void print() {
        cout << "name: " << name << endl;
    }
};

class Macintosh : public computer {
private:
    string color;                 // Macintosh 比 computer 多一个 color
public:
    // 先调用 computer(n) 初始化父类的 name,再初始化自己的 color
    Macintosh(string n, string c) : computer(n), color(c) {}

    void print() {
        cout << "name: " << name << endl;
        cout << "color:" << color << endl;
    }
};
最容易丢分忘记在父类 print 前面加 virtual。不加的话,p->print() 只看 p 的声明类型 computer。
如果题目改了改哪里
类名改成 Phone/iPhone替换类名,继承关系和 virtual 逻辑不变。
多一个成员 price在子类加成员、构造函数参数、print 输出。
输出格式变了只改 cout 里的文字和空格。
第42题

String 类:构造、析构、拷贝、赋值

已知 String 类原型如下,请编写构造函数、析构函数、拷贝构造函数,并重载赋值运算符。
class String {
public:
    String(const char *str = NULL);
    String(const String &other);
    ~String(void);
    String & operator=(const String &other);
private:
    char *m_data;
};
先记住只要类里有 char* 这种指针成员,就要小心深拷贝。不能让两个对象共用同一块字符串内存。
#include <cstring>             // strlen / strcpy

// 普通构造:用 C 字符串创建 String 对象
String::String(const char *str) {
    if (str == NULL) {          // 如果传进来的是空指针
        m_data = new char[1];   // 也要分配 1 个字符空间
        m_data[0] = '\0';       // '\0' 表示空字符串结束
    } else {
        m_data = new char[strlen(str) + 1]; // +1 是给 '\0'
        strcpy(m_data, str);                // 把 str 内容复制进 m_data
    }
}

// 拷贝构造:用另一个 String 创建新 String
String::String(const String &other) {
    m_data = new char[strlen(other.m_data) + 1]; // 给新对象开新内存
    strcpy(m_data, other.m_data);                // 复制内容,不共用地址
}

// 析构:对象销毁时释放自己申请的内存
String::~String(void) {
    delete [] m_data;           // new char[] 必须配 delete[]
}

// 赋值运算符:已有对象 = 另一个已有对象
String& String::operator=(const String &other) {
    if (this == &other) {       // 防止自己给自己赋值
        return *this;
    }

    delete [] m_data;           // 先释放旧内容
    m_data = new char[strlen(other.m_data) + 1]; // 再开新内存
    strcpy(m_data, other.m_data);                // 复制新内容

    return *this;               // 返回自己,支持 a = b = c
}
句子为什么要写
const String &other引用传参避免再次拷贝;const 保证不改 other。
this == &other防自赋值,不然先 delete 自己,再复制自己,会出事。
strlen(...) + 1C 字符串末尾有 '\0',要多留一个位置。
delete [] m_data数组 new 出来的内存,要用 delete[]
第43题

Stack 类模板:后进先出

实现一个 Stack 类模板,包含动态数组 data、栈顶 top、容量 capacity,支持 push、pop、peek、getSize、析构和 operator<< 输出。
top = -1:表示栈是空的。
data[++top] = value:先让 top 上移,再把值放进去。
return data[top--]:先返回栈顶,再让 top 下移。
#include <iostream>
#include <stdexcept>             // underflow_error
using namespace std;

template <typename T>
class Stack {
private:
    T* data;                      // 动态数组,真正存数据的地方
    int top;                      // 栈顶下标;空栈时 top = -1
    int capacity;                 // 当前数组容量

    void resize() {
        capacity *= 2;            // 容量翻倍
        T* newData = new T[capacity]; // 开一块更大的新空间

        for (int i = 0; i <= top; i++) {
            newData[i] = data[i]; // 把旧数据搬过去
        }

        delete[] data;            // 释放旧数组
        data = newData;           // data 指向新数组
    }

public:
    Stack(int initialCapacity = 8) {
        capacity = initialCapacity;
        top = -1;                 // 一开始没有元素
        data = new T[capacity];   // 开初始容量的数组
    }

    void push(T value) {
        if (top + 1 >= capacity) {
            resize();             // 满了就扩容
        }
        data[++top] = value;      // 先 top+1,再放入 value
    }

    T pop() {
        if (top < 0) {
            throw underflow_error("Stack is empty");
        }
        return data[top--];       // 返回栈顶,然后 top-1
    }

    T peek() const {
        if (top < 0) {
            throw underflow_error("Stack is empty");
        }
        return data[top];         // 只看栈顶,不弹出
    }

    int getSize() const {
        return top + 1;           // top 是下标,所以个数是 top+1
    }

    ~Stack() {
        delete[] data;            // 析构时释放动态数组
    }

    friend ostream& operator<<(ostream& os, const Stack<T>& s) {
        for (int i = s.top; i >= 0; i--) { // 从栈顶到栈底
            os << s.data[i];
            if (i > 0) os << " ";
        }
        return os;
    }
};
如果题目改了改哪里
初始容量不是 8改构造函数默认值 initialCapacity = 8
扩容不是翻倍capacity *= 2
输出从栈底到栈顶operator<< 里的循环改成从 0top
空栈不抛异常,返回默认值poppeekthrow 的处理。