C++语言程序设计精要与实践 - 清华大学郑莉教授课件
简介:《C++语言程序设计》由清华大学郑莉教授编著,是一本面向初学者和进阶者的经典教材。第三版课件结合实践与实例,旨在深化读者对C++编程的理解。该教材详细介绍了基础语法、面向对象编程、模板、异常处理、STL、命名空间、函数与运算符重载、内联函数与宏、指针与引用、文件I/O等核心概念。学习本课程后,读者将能够编写高效和可维护的C++代码。 
1. C++基础语法入门
1.1 C++简介及其发展历史
C++是一种静态类型、编译式、通用的编程语言,它支持过程化编程、面向对象编程以及泛型编程。由贝尔实验室的Bjarne Stroustrup在1980年代初期发明,最初被称为C with Classes。C++继承了C语言高效、灵活、简洁等优点,并在此基础上加入了面向对象编程特性,使其在系统软件、游戏开发、高性能服务器和客户端开发等领域得到广泛应用。
1.2 C++程序的基本结构
一个简单的C++程序包含以下几个基本组成部分:
- 预处理指令 :以 # 开头的编译指令,如 #include 用于包含其他文件。
- 主函数 : int main() 是C++程序的入口点。程序执行从这里开始。
- 变量和数据类型 :用于存储数据的基本单位。
- 表达式和语句 :构成程序逻辑的基本元素,用于执行操作并计算值。
- 函数 :执行特定任务的代码块,可以有参数和返回值。
下面是一个C++程序的简单示例:
#include <iostream> // 预处理指令,包含标准输入输出流库
int main() {
std::cout << "Hello, World!" << std::endl; // 输出语句
return 0; // 程序正常退出
}
1.3 C++基础语法精讲
C++基础语法包括了对变量、数据类型、运算符、控制结构和函数的理解和使用。
变量和数据类型
变量是数据存储的基本单位,而数据类型决定了变量可以存储的数据种类。C++中常见的基本数据类型包括 int (整型)、 float (浮点型)、 double (双精度浮点型)、 char (字符型)和 bool (布尔型)。
运算符
运算符用于执行各种类型的操作,例如赋值运算符 = 、算术运算符 + - * / % 、关系运算符 == != > < >= <= 等。
控制结构
控制结构用于控制程序流程,包括条件语句(如 if-else 和 switch )和循环语句(如 for 、 while 和 do-while )。
函数
函数是组织好的、可重复使用的、用来执行特定任务的代码块。函数可以提高代码的重用性,保持代码的模块化。
以上章节内容按照由浅入深的方式介绍了C++的基础语法,为初学者提供了入门知识框架。后续章节将深入探讨面向对象编程、模板编程、异常处理等更高级的主题。
2. 深入理解面向对象编程(OOP)
2.1 面向对象的三大特性
2.1.1 封装、继承与多态的概念和实现
面向对象编程(OOP)的三大特性——封装、继承与多态是构建复杂软件系统的基础。它们之间的交互与协作极大地提高了代码的复用性、灵活性和可维护性。
封装(Encapsulation)
封装是将数据(或状态)和操作数据的代码捆绑在一起的过程,形成一个类。通过提供类的公有接口来隐藏类的内部实现细节,以实现更好的数据安全性和模块化。
在C++中,我们通过定义类来实现封装。类的私有成员(如变量和函数)只能被类的成员函数和友元函数访问。公有成员函数则提供了一个访问接口。
class Account {
private:
std::string owner;
double balance;
public:
Account(const std::string& owner, double balance) : owner(owner), balance(balance) {}
double getBalance() const {
return balance;
}
void deposit(double amount) {
if (amount > 0)
balance += amount;
}
};
在上面的类定义中, owner 和 balance 是私有成员,只能通过公有成员函数 getBalance 和 deposit 来访问和修改。这样就实现了数据封装,防止了外部代码直接修改类的内部状态。
继承(Inheritance)
继承是一个类(子类)可以继承另一个类(父类)的属性和方法的特性。在C++中,使用冒号 : 来表示继承关系,例如:
class SavingAccount : public Account {
public:
SavingAccount(const std::string& owner, double balance) : Account(owner, balance) {}
void applyInterest(double rate) {
if (rate > 0)
deposit(getBalance() * rate / 100);
}
};
在这个例子中, SavingAccount 继承了 Account 类,并添加了一个新的方法 applyInterest 。继承允许 SavingAccount 复用 Account 的代码,减少了重复编写相同代码的工作量。
多态(Polymorphism)
多态是同一个接口使用不同实例而执行不同操作的能力。在C++中,多态主要通过虚函数(virtual functions)和基类指针或引用实现。
class Base {
public:
virtual void doSomething() {
// ...
}
};
class Derived : public Base {
public:
void doSomething() override {
// ...
}
};
Base* basePtr = new Derived();
basePtr->doSomething(); // 调用Derived的doSomething
在这个例子中, Derived 类继承了 Base 类,并且覆盖了 doSomething 函数。通过基类指针 Base* 可以调用 doSomething 函数,实际运行时调用的是 Derived 类中的版本,这就是多态的体现。
2.1.2 类与对象的关系及其应用
在OOP中,类是创建对象的模板,对象是类的实例。理解类和对象的关系对于编写高效和可维护的代码至关重要。
类的定义
类定义了对象将拥有哪些属性和方法。例如:
class Date {
private:
int year, month, day;
public:
Date(int y, int m, int d) : year(y), month(m), day(d) {}
void print() const {
// ...
}
};
在这个 Date 类中,我们定义了三个私有成员变量来存储日期信息,以及一个构造函数来初始化这些变量。 print 成员函数用于输出日期信息。
对象的创建
对象是根据类定义创建的实例。通过构造函数,对象的属性被初始化,它成为可以在程序中使用的实体。
Date today(2023, 3, 14);
today.print(); // 使用对象调用print方法
类与对象的关系应用
在实际应用中,类和对象的使用涉及到设计模式、数据结构的实现以及程序的模块化。
- 设计模式 :比如工厂模式、单例模式等,通过类和对象的特定组织来解决软件开发中的常见问题。
- 数据结构 :链表、栈、队列等数据结构在C++中通常是通过类来实现的,它们的实例(对象)是这些数据结构的具体表现。
- 程序模块化 :通过类将程序的不同功能封装成模块,提高了代码的重用性和可读性。
在理解了类和对象的定义以及如何创建和使用它们之后,我们可以更深入地探讨面向对象设计原则以及如何在C++中实现它们。这是面向对象编程中至关重要的部分,有助于我们构建健壮、灵活且易于维护的软件系统。
2.2 面向对象设计原则
面向对象设计原则是指导我们如何在面向对象编程中合理地组织代码的规范和最佳实践。它们帮助我们编写出更加清晰、模块化和可扩展的代码,这些原则在软件开发领域具有广泛的认可。
2.2.1 SOLID原则解析与案例分析
SOLID是一个由五个面向对象设计原则的首字母组成的缩写,它们分别代表:
- 单一职责原则(Single Responsibility Principle, SRP)
- 开闭原则(Open/Closed Principle, OCP)
- 里氏替换原则(Liskov Substitution Principle, LSP)
- 接口隔离原则(Interface Segregation Principle, ISP)
- 依赖倒置原则(Dependency Inversion Principle, DIP)
单一职责原则
单一职责原则指出,一个类应该只有一个引起变化的原因。这意味着一个类应该只有一个功能或责任。这有助于保持类的内聚性,避免因需求变化导致类的过度修改。
class Logger {
public:
void log(const std::string& message) {
// 日志记录功能
}
};
class FileLogger : public Logger {
public:
void log(const std::string& message) override {
// 文件日志记录实现
}
};
在上面的例子中, Logger 类负责日志记录功能,而 FileLogger 继承自 Logger 并实现具体的文件日志记录。这满足了单一职责原则,因为 FileLogger 类有一个明确的职责。
开闭原则
开闭原则主张软件实体应当对扩展开放,对修改关闭。这意味着当需求变化时,我们应该通过添加新的代码来满足新需求,而不是修改现有的代码。
class Shape {
public:
virtual double area() const = 0;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
};
// 如果需求变化,添加新的形状类,无需修改现有代码
class Rectangle : public Shape {
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
};
里氏替换原则
里氏替换原则指出,派生类(子类)对象必须能够替换其基类(父类)对象。这意味着所有的子类应该增强而不是削弱父类的功能。
void processShape(Shape& s) {
double a = s.area();
// 使用面积进行一些处理
}
// Rectangle和Circle都可以作为Shape的实例传递给processShape
接口隔离原则
接口隔离原则主张多个专门的接口比单一的总接口要好。这有助于实现更小的、职责单一的接口,从而减少类之间不必要的依赖。
class Print {
public:
virtual void print() = 0;
};
class Scan {
public:
virtual void scan() = 0;
};
class PrintScan : public Print, public Scan {
public:
void print() override {}
void scan() override {}
};
依赖倒置原则
依赖倒置原则要求高层模块不应依赖于低层模块,两者都应依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。这意味着我们应该使用接口或抽象基类来降低模块之间的耦合度。
class IRenderer {
public:
virtual void render() = 0;
};
class OpenGLRenderer : public IRenderer {
public:
void render() override {
// 实现OpenGL渲染
}
};
class Application {
IRenderer& renderer;
public:
Application(IRenderer& r) : renderer(r) {}
void useRenderer() {
renderer.render();
}
};
在这个例子中, Application 类依赖于 IRenderer 接口,而不依赖于具体实现,如 OpenGLRenderer 。这符合依赖倒置原则,因为高层模块 Application 不直接依赖于低层模块的实现。
通过遵守这些SOLID原则,我们可以确保我们的软件设计是灵活的、可扩展的,并且易于维护。在实际项目中,这通常是通过重构现有的代码来实现的,以满足上述原则的要求。在下一节中,我们将探索设计模式在C++中的实现与应用,这是面向对象编程中一个重要的主题。
2.2.2 设计模式在C++中的实现与应用
设计模式是针对特定问题的解决方案的模板,它们是对软件设计中重复出现的问题的总结和归纳。设计模式提供了一套经过验证的代码结构,可以帮助开发者构建更加灵活和可维护的代码。C++作为一门强大的面向对象语言,为设计模式的实现提供了丰富的特性。
在C++中实现设计模式,我们通常会利用到类的继承、组合、多态以及模板编程等特性。下面,我们将探讨几种常见设计模式在C++中的实现方式。
工厂模式
工厂模式是一种创建型设计模式,提供了一种创建对象的最佳方式。它通常与抽象基类配合使用,使得客户端代码可以创建子类对象而无需直接实例化子类。
class Product {
public:
virtual void doSomething() = 0;
};
class ConcreteProduct : public Product {
public:
void doSomething() override {
// 具体实现
}
};
class Creator {
public:
Product* factoryMethod() {
return new ConcreteProduct();
}
};
int main() {
Creator c;
Product* p = c.factoryMethod(); // 使用工厂方法创建对象
p->doSomething();
}
观察者模式
观察者模式定义了对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新。
class Observer {
public:
virtual void update(int val) = 0;
};
class Subject {
private:
std::vector<Observer*> observers;
int value;
public:
void attach(Observer* o) {
observers.push_back(o);
}
void setValue(int val) {
value = val;
notify();
}
void notify() {
for (auto* o : observers) {
o->update(value);
}
}
};
class ConcreteObserver : public Observer {
public:
void update(int val) override {
// 根据更新值执行操作
}
};
策略模式
策略模式定义了算法的家族,将每个算法封装起来,并使它们可以互换。策略模式让算法的变化独立于使用算法的客户。
class Context {
public:
void setStrategy(Strategy* s) {
strategy = s;
}
void executeStrategy() {
strategy->algorithmInterface();
}
private:
Strategy* strategy;
};
class ConcreteStrategyA : public Strategy {
public:
void algorithmInterface() override {
// 执行算法A
}
};
class ConcreteStrategyB : public Strategy {
public:
void algorithmInterface() override {
// 执行算法B
}
};
在C++中实现设计模式时,我们需要考虑语言的特性,如类的构造与析构时机、指针与引用的使用、异常安全性等问题。设计模式不仅仅是一些固定代码的模式,更是软件设计思维的体现。合理使用设计模式,可以让我们编写出更加优雅、清晰且易于维护的代码。
通过深入理解面向对象编程的三大特性以及面向对象设计原则和设计模式,我们不仅能够编写出符合面向对象范式的设计,还能够解决实际问题,提高软件的可维护性和可扩展性。这为我们在后续章节深入探讨C++的模板编程、异常处理以及高级特性奠定了坚实的基础。
3. C++模板编程的艺术
模板编程是C++中一种高级特性,它提供了一种在编译时进行泛型编程的方式,使得代码可以更加通用和重用。本章将探讨C++模板的基础知识和高级特性,以及如何有效地使用模板提升编程效率和质量。
3.1 模板的基础知识
3.1.1 函数模板的定义与使用
函数模板允许编写与数据类型无关的函数,这样的函数可以在编译时生成特定类型的代码。定义函数模板通常以关键字 template 开始,后面跟着一个或多个模板参数。
下面是一个简单的函数模板示例,它用于交换两个变量的值:
template <typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
在这个例子中, T 是一个模板类型参数,它在编译时会被实际类型所替换。函数模板可以通过不同的数据类型多次实例化,例如 swap<int> 或 swap<double> 。
使用说明:
调用模板函数时,编译器会根据实际传递的参数类型来实例化对应的模板函数。例如:
int main() {
int x = 5, y = 10;
swap(x, y); // 编译器实例化为 swap<int>(int&, int&)
double d1 = 3.14, d2 = 2.71;
swap(d1, d2); // 编译器实例化为 swap<double>(double&, double&)
return 0;
}
3.1.2 类模板的声明与实例化
类模板允许创建与类型无关的类。与函数模板类似,类模板也使用 template 关键字来定义模板参数。
下面是一个简单的类模板示例,它定义了一个通用的数组类:
template <typename T>
class Array {
private:
T* m_data;
size_t m_size;
public:
Array(size_t size) : m_size(size), m_data(new T[size]) {}
~Array() {
delete[] m_data;
}
T& operator[](size_t index) {
return m_data[index];
}
const T& operator[](size_t index) const {
return m_data[index];
}
};
在这个例子中, Array 是一个模板类,它有一个模板类型参数 T ,用于定义数据类型的数组。
实例化说明:
类模板的实例化是在创建类对象时,显式或隐式地指定模板参数完成的。例如:
int main() {
Array<int> intArray(10); // 隐式实例化
Array<double> doubleArray(20); // 隐式实例化
intArray[0] = 1;
doubleArray[0] = 1.0;
return 0;
}
3.2 模板高级特性
3.2.1 模板特化与重载
模板特化允许我们为特定类型提供特殊的实现,这在默认模板实现不适用或效率不高时非常有用。模板重载则是指根据参数的不同,定义多个同名模板函数。
下面展示了函数模板特化和重载的例子:
// 原始模板定义
template <typename T>
void process(T& value);
// 特化版本
template <>
void process(const char*& value);
// 重载版本
template <typename T>
void process(T* value);
在上述代码中, process 函数对于普通类型使用了原始模板定义,对于 const char* 类型提供了特化版本,对于指针类型提供了重载版本。
3.2.2 非类型模板参数与模板元编程
非类型模板参数允许我们传递编译时常量值给模板,这样的参数可以用于数组大小、枚举或指针等。模板元编程是指在编译时期执行的编程。
下面是一个使用非类型模板参数的例子:
template <typename T, size_t N>
class FixedArray {
private:
T m_data[N];
public:
// ...
};
在这个例子中, FixedArray 类模板有一个非类型模板参数 N ,它用于定义数组的大小。
模板元编程示例:
模板元编程可以用来在编译时期计算数值,例如使用递归模板结构实现编译时期的阶乘计算:
template <unsigned int n>
struct Factorial {
enum { value = n * Factorial<n - 1>::value };
};
template <>
struct Factorial<0> {
enum { value = 1 };
};
int main() {
int result = Factorial<5>::value; // 结果为 120
return 0;
}
这段代码定义了一个编译时阶乘计算的模板元程序,其中 Factorial<0> 提供了递归的终止条件。
3.3 模板编程的优化技巧
模板编程是一种强大的特性,但也需要仔细设计以避免不必要的编译开销。下面是一些模板编程的优化技巧:
- 尽量使用非类型模板参数,这样可以减少函数模板的实例化次数,减少代码膨胀。
- 通过特化来优化性能,为常见的类型提供特化版本,可以避免不必要的类型转换和提升效率。
- 尽可能减少模板代码的复杂度,复杂模板可能导致编译时间的增加。
- 使用模板元编程时要小心,因为它的计算是在编译时完成的,可能会导致编译时间显著增长。
模板编程是C++提供的一种强大工具,它允许开发者编写通用、灵活的代码。掌握模板的基础知识和高级特性对于编写高效的C++程序至关重要。
4. 异常处理机制与标准模板库(STL)
4.1 C++异常处理机制
异常处理是现代编程语言不可或缺的一部分,它提供了一种结构化的方法来处理程序运行时出现的意外情况。C++中的异常处理机制使用 try , catch , 和 throw 关键字来管理错误。
4.1.1 try-catch结构的工作原理
try 块用于包围一段可能抛出异常的代码。当在 try 块内发生异常时,C++运行时系统会搜索相应的 catch 块来处理该异常。如果没有匹配的 catch 块,程序将调用 std::terminate 并异常终止。
try {
// 可能抛出异常的代码
} catch (const std::exception& e) {
// 处理std::exception及其派生类异常
} catch (...) {
// 捕获所有其他类型的异常
}
throw 语句用于抛出异常对象。如果在 throw 后没有跟任何对象,则表示重新抛出当前捕获的异常。
4.1.2 异常安全编程的实践技巧
异常安全指的是程序在抛出异常时能够保持合理的状态,并且资源不会泄露。异常安全有三个基本保证:
- 基本保证:程序不会崩溃,对象处于有效但可能不确定的状态。
- 强烈保证:程序保持在抛出异常之前的状态,或者转移到一个安全的状态。
- 不抛出保证:承诺不抛出异常。
为了实现这些保证,开发者可以使用RAII(Resource Acquisition Is Initialization)技术管理资源,使用智能指针等。
4.1.3 错误处理的现代实践
现代C++建议使用异常来处理错误,而不是返回错误码。但在性能关键部分,例如底层库中,错误码可能更合适。
void MyFunction() {
if (/* 错误条件 */) {
throw std::runtime_error("错误信息");
}
// 正常逻辑
}
4.1.4 示例分析
考虑一个简单的例子,其中可能抛出异常:
#include <iostream>
#include <stdexcept>
void riskyOperation() {
throw std::runtime_error("发生错误");
}
int main() {
try {
riskyOperation();
} catch (const std::runtime_error& e) {
std::cerr << "捕获异常:" << e.what() << '\n';
}
return 0;
}
4.1.5 总结
异常处理是C++不可或缺的一部分,为错误处理提供了强大的工具。理解 try , catch , 和 throw 的工作方式是编写可靠C++代码的关键。实践异常安全编程,并根据现代C++实践调整错误处理策略,将帮助开发者构建健壮的软件系统。
4.2 标准模板库(STL)详解
STL是C++标准库的核心,它提供了一系列的容器、迭代器、算法以及函数对象。它极大地简化了C++编程,使得开发者能够高效地处理数据集合。
4.2.1 STL容器的种类与选择
STL容器是管理对象集合的数据结构。主要容器类型包括 vector , list , deque , set , map 等。
vector:动态数组,支持快速随机访问,但在尾部之外的位置插入和删除元素时效率较低。list:双向链表,支持高效插入和删除操作,但在任意位置查找元素效率较低。deque:双端队列,可以在两端进行快速的插入和删除操作。set与multiset:红黑树实现,自动排序,不允许重复元素,支持高效的查找、插入和删除操作。map与multimap:键值对集合,基于红黑树实现,支持快速查找。
选择哪个容器取决于应用场景和性能要求。例如,需要频繁随机访问元素时应选择 vector ,如果需要快速插入和删除,则选择 list 或 deque 。
4.2.2 STL迭代器与算法的应用
迭代器是STL的核心概念之一,它提供了一种统一的方式来遍历不同类型的容器。迭代器行为类似于指针。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
for (std::vector<int>::iterator it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << ' ';
}
std::cout << std::endl;
return 0;
}
STL算法是一组用于操作容器内容的函数模板。这些算法并不直接操作容器,而是通过迭代器操作容器中的元素。例如:
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::sort(vec.begin(), vec.end());
for (int elem : vec) {
std::cout << elem << ' ';
}
std::cout << std::endl;
return 0;
}
4.2.3 STL中的适配器、函数对象与仿函数
适配器是能够修改现有容器或算法接口的组件。例如 stack 和 queue 是基于 deque 的适配器,它们提供了不同的接口。
函数对象(Functors)是可以像函数一样被调用的对象。在STL中,它们经常被用作算法的参数。
#include <algorithm>
#include <vector>
class MyFunctor {
public:
void operator()(int x) {
// 操作
}
};
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(vec.begin(), vec.end(), MyFunctor());
}
4.2.4 总结
STL是C++开发的强大工具,提供丰富的容器、算法和迭代器。了解如何选择合适的容器、使用迭代器和算法以及利用函数对象可以大幅提升开发效率和代码质量。适配器、函数对象和仿函数为STL的扩展性和灵活性提供了基础。掌握STL是成为C++高级程序员的必经之路。
5. C++高级特性与实践应用
5.1 命名空间与作用域
5.1.1 命名空间的定义与使用
在C++中,命名空间(Namespace)是一种封装事物的方法,用来组织代码,避免名称冲突。命名空间是通过关键字 namespace 来定义的,其中可以包含类、函数、变量等。
namespace FirstNamespace {
void function() {
std::cout << "Inside FirstNamespace" << std::endl;
}
}
namespace SecondNamespace {
void function() {
std::cout << "Inside SecondNamespace" << std::endl;
}
}
int main() {
FirstNamespace::function();
SecondNamespace::function();
return 0;
}
命名空间可以嵌套使用,也可以使用 using 关键字来简化代码。
using namespace FirstNamespace;
function();
使用命名空间可以更清晰地管理代码,特别是在大型项目中,不同的开发团队可以使用不同的命名空间来避免类名或函数名的冲突。
5.1.2 作用域规则及其在代码组织中的应用
在C++中,作用域是变量、函数、类等实体在代码中可以被访问到的区域。根据作用域的不同,可以分为局部作用域、类作用域、命名空间作用域和全局作用域。
int globalVar = 10; // 全局变量
void function() {
int localVar = 5; // 局部变量
std::cout << "Global Var: " << globalVar << std::endl;
std::cout << "Local Var: " << localVar << std::endl;
}
int main() {
function();
return 0;
}
在上面的例子中, globalVar 是全局变量,它在全局作用域内可以被访问。 localVar 是局部变量,它只在 function() 函数的作用域内可以被访问。
正确地使用作用域可以使代码更加清晰,易于理解和维护。例如,利用局部作用域可以避免全局变量污染,使用类作用域可以封装类成员,而命名空间则可以有效地组织跨多个文件的代码。
5.2 函数与运算符的扩展使用
5.2.1 函数重载的原理与实践
函数重载是C++支持的一个特性,允许创建多个同名函数,但它们的参数列表不同。编译器根据函数调用时提供的参数列表来区分应该调用哪个函数。
void print(int num) {
std::cout << "Printing int: " << num << std::endl;
}
void print(double num) {
std::cout << "Printing float: " << num << std::endl;
}
void print(const std::string& s) {
std::cout << "Printing string: " << s << std::endl;
}
int main() {
print(123); // Calls print(int)
print(123.45); // Calls print(double)
print("Hello"); // Calls print(const std::string&)
return 0;
}
在实践中,函数重载可以使代码更易用,例如 std::cout 重载了输出操作符 << ,以处理不同类型的数据。
5.2.2 运算符重载的设计与案例
运算符重载允许为现有的C++运算符赋予用户定义的类型,可以自定义其操作的行为。运算符重载是通过在类中定义特定的成员函数或友元函数来实现的。
class Complex {
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}
// 重载+运算符
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 输出运算符重载
friend std::ostream& operator<<(std::ostream& out, const Complex& obj);
};
std::ostream& operator<<(std::ostream& out, const Complex& obj) {
out << obj.real << " + " << obj.imag << "i";
return out;
}
int main() {
Complex c1(2.0, 3.0);
Complex c2(3.0, 4.0);
Complex sum = c1 + c2;
std::cout << "Sum: " << sum << std::endl;
return 0;
}
在这个例子中,我们定义了一个 Complex 类来表示复数,并重载了 + 运算符来实现复数加法。同时,我们也重载了 << 运算符,以便于直接将复数输出到标准输出流中。运算符重载提高了代码的可读性和易用性。
5.3 C++内存管理
5.3.1 指针与引用的本质区别与使用技巧
在C++中,指针(pointer)和引用(reference)都提供了访问变量地址的方式,但它们的语法和使用场景有所不同。
指针是一个变量,其值是另一个变量的地址,因此指针的值可以更改,让它指向另一个地址。
int value = 10;
int *ptr = &value; // ptr 指向 value 的地址
*ptr = 20; // 通过指针修改 value 的值
std::cout << "Value: " << value << std::endl; // 输出 Value: 20
引用是一个别名,一旦初始化后,就始终引用着同一个对象。
int &ref = value; // ref 是 value 的引用
ref = 30; // 通过引用修改 value 的值
std::cout << "Value: " << value << std::endl; // 输出 Value: 30
指针的使用更加灵活,但引用在某些情况下使用起来更安全,因为引用必须在创建时初始化,并且不能更改它所引用的对象。在实际开发中,引用经常用作函数参数,以避免对象的拷贝,而指针则用于动态内存分配和指针算术。
5.3.2 内联函数的优势与预处理器宏的使用
内联函数是一种在编译时将函数体直接插入调用位置的函数,用于替代预处理器宏,以消除函数调用开销,同时保持代码的清晰和类型安全。
inline int max(int a, int b) {
return (a > b) ? a : b;
}
int main() {
int m = max(5, 10);
std::cout << "Maximum value: " << m << std::endl;
}
内联函数的优势在于减少了函数调用的开销,特别是当函数体非常小的时候。然而,过度使用内联函数可能会导致代码膨胀,因此需要谨慎使用。
预处理器宏是一种在编译前进行文本替换的指令,常用于定义常量或简单的函数。
#define SQUARE(x) ((x) * (x))
int main() {
int sq = SQUARE(4);
std::cout << "Square of 4: " << sq << std::endl;
}
尽管预处理器宏可以提供简单的代码替换功能,但它不会检查类型安全,可能导致错误。相比之下,内联函数在编译时检查类型,并且能够处理更复杂的逻辑。
5.4 文件I/O操作的深入学习
5.4.1 文件流的打开、读写与关闭操作
C++标准库提供了 <fstream> 头文件中定义的三个主要的文件流类: ifstream 、 ofstream 和 fstream ,用于处理文件输入、输出和输入/输出操作。
#include <fstream>
int main() {
std::ofstream outFile("example.txt");
if (outFile.is_open()) {
outFile << "Writing to a file.\n";
outFile.close();
}
std::ifstream inFile("example.txt");
if (inFile.is_open()) {
std::string line;
while (getline(inFile, line)) {
std::cout << line << '\n';
}
inFile.close();
}
}
在上面的代码中, outFile 是一个 ofstream 对象,用于向文件写入内容。 inFile 是一个 ifstream 对象,用于从文件中读取内容。文件的打开和关闭是通过调用 is_open() 、 open() 和 close() 方法来完成的。 getline() 函数用于读取文件的每一行。
5.4.2 高级文件操作技巧与异常处理
除了基本的读写操作外,C++还支持一些高级的文件操作技巧,如二进制模式读写、文件位置指针操作和异常处理。
#include <fstream>
#include <iostream>
int main() {
std::ofstream outFile("example.bin", std::ios::binary);
if (outFile.is_open()) {
int n = 10;
outFile.write(reinterpret_cast<char*>(&n), sizeof(n));
outFile.close();
}
std::ifstream inFile("example.bin", std::ios::binary);
if (inFile.is_open()) {
int n;
inFile.read(reinterpret_cast<char*>(&n), sizeof(n));
std::cout << "Read: " << n << std::endl;
inFile.close();
}
}
在此示例中,我们以二进制模式打开文件,使用 write() 和 read() 函数读写二进制数据。异常处理可以通过检查 good() 或 fail() 方法来进行。
try {
// 文件操作代码
} catch(std::ios_base::failure &e) {
std::cerr << "I/O error: " << e.what() << std::endl;
}
异常处理为文件操作提供了健壮性,可以有效捕捉文件读写过程中可能出现的错误,例如磁盘空间不足、文件无法打开等情况。
本章节介绍C++的高级特性,包括命名空间和作用域的管理、函数与运算符的扩展使用、内存管理以及文件I/O操作的深入学习。这些知识点对于C++开发者在进行更高级的应用开发时,提供了必不可少的工具和方法。在实际开发中,熟练使用这些高级特性可以极大提高代码的质量和开发效率。
简介:《C++语言程序设计》由清华大学郑莉教授编著,是一本面向初学者和进阶者的经典教材。第三版课件结合实践与实例,旨在深化读者对C++编程的理解。该教材详细介绍了基础语法、面向对象编程、模板、异常处理、STL、命名空间、函数与运算符重载、内联函数与宏、指针与引用、文件I/O等核心概念。学习本课程后,读者将能够编写高效和可维护的C++代码。
更多推荐

所有评论(0)