简介:《C++程序设计》是北京大学计算机系常宝宝教授编写的课件,旨在为C++初学者提供全面和深入的基础知识。本课程覆盖了C++的核心概念和技巧,包括基本语法、函数、指针、类与对象、构造与析构函数、运算符重载、动态内存管理、模板、异常处理和标准库使用等。同时,"数据结构(c++)"子文件深入讲解了C++在数据结构方面的应用,帮助学生掌握链表、栈、队列、树、图等数据结构的实现及优化。学习完成后,学生将能解决实际问题,并为深入学习计算机科学打下坚实基础。
1. C++基础语法学习
简介
C++是一种静态类型、编译式、通用的编程语言,广泛应用于系统/应用软件开发、游戏开发、驱动程序、高性能服务器和客户端开发。掌握C++的基础语法是成为有效C++开发者的关键第一步。
关键元素概览
C++的基础语法包含了变量声明、数据类型、运算符、控制结构(如循环和条件语句)、函数的定义和调用等关键元素。此外,还包括了对面向对象编程(OOP)的基础支持,如类和对象的创建。
#include <iostream>
int main() {
// 声明并初始化一个整型变量
int num = 10;
// 使用cout输出变量
std::cout << "The number is: " << num << std::endl;
return 0;
}
如上述代码所示,声明了一个整型变量 num 并使用标准输出流 cout 输出了这个变量的值。这是学习C++基础语法的简单起点。通过这个例子,可以初步了解C++的基本语法结构和输出语句的使用。
在后续章节中,我们将逐步深入探讨C++语法的各个方面,帮助读者建立起对C++语言深刻的理解。
2. 函数的定义和使用
2.1 函数的基本概念
2.1.1 函数的声明和定义
在C++中,函数是一段代码的封装,它执行特定的任务,并可以通过调用来执行。函数的声明和定义是构建函数的基本步骤。声明告诉编译器函数的存在及其相关参数和返回类型,而定义则是函数体的具体实现。
// 函数声明
int max(int, int); // 声明了一个名为max的函数,有两个int型参数,返回int型结果
// 函数定义
int max(int a, int b) { // 定义了一个名为max的函数,接受两个int型参数
return a > b ? a : b; // 实现了返回两个参数中较大值的逻辑
}
函数声明通常放在头文件中,以便其他文件包含该头文件时可以使用该函数。而函数定义则放在源文件中,编译器在编译时会根据函数声明的接口生成相应的调用代码。
2.1.2 参数传递方式及其影响
C++中函数参数的传递方式有两种:值传递和引用传递。值传递会创建一个参数的副本,而引用传递则传递参数的引用,允许函数修改实际参数的值。
void square(int num); // 值传递版本,不改变原参数值
void square(int &num); // 引用传递版本,可以改变原参数值
通过引用传递可以避免不必要的数据复制,特别是在处理大型对象或数组时,可以提高程序效率。值传递则适用于不需要修改原参数值的情况,或者当参数为简单数据类型时。
2.2 函数的高级特性
2.2.1 默认参数和函数重载
C++允许在函数声明时设置默认参数,这样在调用函数时,如果缺少某些参数,编译器会使用默认值。函数重载允许有多个同名函数,只要它们的参数列表不同即可。
void print(const char* str = "default message"); // 默认参数示例
void print(int num); // 函数重载示例
// 调用函数
print(); // 使用默认参数
print("hello world"); // 明确指定参数
print(10); // 调用函数重载版本
默认参数和函数重载的使用让函数的接口更加灵活和方便。但要注意不要造成重载函数间的歧义,否则编译器无法判断该调用哪个函数。
2.2.2 内联函数和递归函数
内联函数是C++中一种特殊的函数,编译器在编译时会将内联函数的代码直接嵌入到调用它的代码中,以减少函数调用开销。而递归函数是自己调用自己的函数,通常用于解决可以分解为相似子问题的问题。
// 内联函数示例
inline int square(int x) { return x * x; }
// 递归函数示例
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
内联函数适用于函数体较小,且频繁调用的场景。递归函数则需要确保有一个明确的退出条件,防止栈溢出。在使用递归时,通常会结合尾递归优化来提高递归效率。
2.3 函数实践
2.3.1 创建和调用函数
创建函数需要定义函数的签名(包括返回类型、函数名和参数列表)并提供函数体。调用函数则是通过函数名和相应的参数列表来执行函数体。
#include <iostream>
// 定义一个函数,计算两个整数的和
int sum(int a, int b) {
return a + b;
}
int main() {
int result = sum(3, 4); // 调用sum函数,并获取返回值
std::cout << "The sum is: " << result << std::endl; // 输出结果
return 0;
}
在上述代码中, sum 函数被定义在main函数之前,它接受两个整数参数,并返回它们的和。在main函数中, sum 函数被调用,并通过标准输出显示结果。
2.3.2 函数指针的使用
函数指针是指向函数的指针变量,通过函数指针可以间接调用函数。
#include <iostream>
// 定义一个函数,计算两个整数的乘积
int multiply(int a, int b) {
return a * b;
}
int main() {
int (*funcPtr)(int, int) = multiply; // 声明并初始化函数指针
int result = funcPtr(4, 5); // 通过函数指针调用multiply函数
std::cout << "The product is: " << result << std::endl; // 输出结果
return 0;
}
函数指针在实现回调函数、设计函数表等场合非常有用,可以增加程序的灵活性。
2.3.3 递归函数的注意事项
递归函数在编写时需要特别注意终止条件的设置和递归深度的控制。合适的终止条件可以防止无限递归的发生,而合理的递归深度则是为了防止栈溢出。
// 计算斐波那契数列的第n项
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
int n = 10;
std::cout << "Fibonacci of " << n << " is: " << fibonacci(n) << std::endl;
return 0;
}
递归算法虽然简洁,但递归过深可能造成栈溢出,并且执行效率较低。在实际开发中,应该对递归进行优化,例如使用尾递归优化或转换为迭代算法。
3. 指针的运用和管理
指针是C++语言的核心概念之一,它的存在使得C++能够以一种灵活的方式处理内存和对象。掌握指针对于理解内存管理、动态数据结构以及高效的资源管理至关重要。
3.1 指针基础
3.1.1 指针的定义和操作
指针本质上是一个变量,存储的是内存地址。这个地址指向一个特定类型的数据。指针的声明通常遵循 type *pointer_name; 的格式。
int value = 5;
int *ptr = &value; // 'ptr' 指向 'value' 的地址
在上述代码中, ptr 是一个指向 int 类型数据的指针。使用 & 运算符取得 value 的地址,并将这个地址赋值给指针 ptr 。
3.1.2 指针与数组的关系
指针与数组之间存在着紧密的联系。数组名在大多数情况下会被解释为指向数组首元素的指针。
int arr[] = {10, 20, 30, 40};
int *ptr = arr; // 'ptr' 指向 'arr' 的第一个元素
当指针 ptr 指向数组时,可以通过指针算术来访问数组元素。
3.2 高级指针概念
3.2.1 指针的指针(多重指针)
指针的指针,或称为双重指针,是一个指向指针的指针。其声明格式为 type **pointer_to_pointer 。
int value = 10;
int *ptr = &value;
int **pptr = &ptr; // 'pptr' 指向 'ptr'
双重指针在动态二维数组、动态分配的指针数组等场景中非常有用。
3.2.2 动态内存分配与指针
C++中通过 new 和 delete 运算符进行动态内存分配和释放。
int *ptr = new int; // 动态分配一个整数的内存
*ptr = 10; // 对动态分配的内存赋值
delete ptr; // 释放动态分配的内存
动态内存管理是灵活的,但也需要谨慎使用。因为忘记释放内存或者释放已经释放的内存会导致内存泄漏或其他安全问题。
本章节的介绍涵盖了指针的基础和高级特性,以及指针与数组之间的关系。在继续深入学习之前,建议对指针的使用和管理有一个扎实的理解,因为这是深入理解C++编程的基石之一。在下一章节中,我们将探讨类和对象,这将引入面向对象编程的全新视角,从而扩展我们对C++能力的认知。
4. 面向对象编程:类与对象
4.1 类的定义和对象的创建
4.1.1 类的封装性和成员函数
面向对象编程(OOP)的核心是类的概念,类可以定义为具有相同属性和行为的实体的模板。在C++中,类是用户自定义的数据类型,它将数据成员和成员函数封装在一起,形成一个逻辑单元。封装性的目的是隐藏对象的内部实现细节,只保留有限的接口与外界交互,这样可以提高代码的安全性和可维护性。
类的封装性体现在私有成员和公共成员的划分上。私有成员只能在类的内部被访问,而公共成员则可以被类的外部代码直接访问。成员函数是定义在类内部的函数,它能够操作类的私有数据。
下面是一个简单的类定义及其成员函数的示例:
class Car {
private:
std::string color; // 私有属性,存储车辆颜色
public:
// 公共构造函数,初始化车辆颜色
Car(const std::string& color) : color(color) {}
// 公共成员函数,返回车辆颜色
std::string GetColor() const {
return color;
}
};
在上述代码中, color 是一个私有成员变量,它只能在 Car 类的内部被访问。 Car 类包含一个构造函数和一个 GetColor 成员函数,都是公共的。构造函数用于创建 Car 类型的对象并初始化私有变量, GetColor 函数则提供了一种方式让外界可以安全地获取 color 变量的值。
4.1.2 构造函数和析构函数的使用
构造函数和析构函数是类中的特殊成员函数,用于在创建和销毁对象时自动执行特定的操作。
构造函数的特点如下: - 名称与类名相同。 - 没有返回类型。 - 可以有参数,用于初始化对象的状态。 - 如果没有明确定义,编译器会生成一个默认的构造函数。
析构函数的特点如下: - 名称是类名前加上波浪号(~)。 - 没有返回类型,也没有参数。 - 不能重载,每个类只能有一个析构函数。 - 当对象的生命周期结束时,析构函数会自动被调用。
以下是一个包含构造函数和析构函数的类示例:
class Dog {
private:
std::string name;
public:
// 构造函数
Dog(const std::string& n) : name(n) {
std::cout << "A dog named " << name << " was created.\n";
}
// 析构函数
~Dog() {
std::cout << "Dog " << name << " is gone.\n";
}
};
在这个例子中, Dog 类有两个成员函数:一个构造函数和一个析构函数。构造函数接受一个字符串参数用于初始化 Dog 对象的名字,并在控制台输出创建该对象的信息。析构函数则输出对象销毁的信息。每次创建 Dog 对象时,构造函数都会被调用,而每次对象离开作用域(例如函数结束)时,析构函数都会被自动执行。
4.2 继承与多态
4.2.1 继承的实现和访问控制
继承是面向对象程序设计中的一个重要特性,它允许一个类继承另一个类的属性和方法,从而实现代码的重用和扩展性。在C++中,继承使用冒号(:)来实现,并且可以定义访问控制符来控制基类成员的继承方式,如 public 、 protected 和 private 。
-
public继承:基类的公有成员和保护成员将保持原有的访问权限;基类的私有成员仍然不可访问。 -
protected继承:基类的公有和保护成员都会变成派生类的保护成员。 -
private继承:基类的公有和保护成员都会变成派生类的私有成员。
下面展示了使用不同访问控制的继承示例:
class Animal {
public:
void eat() { std::cout << "I can eat.\n"; }
protected:
std::string name;
private:
int age;
};
class Dog : public Animal {
public:
void bark() { std::cout << "Woof!\n"; }
};
class SecretDog : private Animal {
public:
void reveal() {
eat(); // 可以访问
// bark(); // 编译错误,因为 Dog::bark 是私有的
}
};
int main() {
Dog dog;
dog.eat(); // 可以访问 public 继承的 eat() 方法
SecretDog secretDog;
secretDog.eat(); // 无法访问 private 继承的 eat() 方法
}
在上述代码中, Dog 类通过 public 继承了 Animal 类,因此可以访问 Animal 的公有方法 eat() 。而 SecretDog 类以 private 方式继承了 Animal ,所以在 SecretDog 的公有方法中, eat() 方法仍然是可访问的,但不能访问从 Dog 类继承的 bark() 方法,因为它属于 Dog 的私有方法。
4.2.2 虚函数和多态的运用
多态是面向对象编程中允许不同类的对象对同一消息做出响应的能力。多态通过在基类中使用虚函数来实现。虚函数允许派生类覆盖基类中的函数实现,从而实现行为的动态绑定。
使用 virtual 关键字声明的基类函数称为虚函数,派生类中可以重写这个函数。当通过基类指针或引用调用函数时,将根据实际对象的类型调用相应的函数实现。
这里是一个多态的实现示例:
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing a shape.\n";
}
};
class Circle : public Shape {
public:
void draw() const override { // 使用 override 关键字更明确表示重写
std::cout << "Drawing a circle.\n";
}
};
class Square : public Shape {
public:
void draw() const override {
std::cout << "Drawing a square.\n";
}
};
void drawShapes(const std::vector<Shape*>& shapes) {
for (const auto& shape : shapes) {
shape->draw(); // 使用虚函数 draw 实现多态
}
}
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Square());
drawShapes(shapes);
// 清理动态分配的内存
for (auto shape : shapes) {
delete shape;
}
}
在这个例子中, Shape 是一个基类,并定义了一个虚函数 draw() 。 Circle 和 Square 类继承自 Shape 并重写了 draw() 函数。 drawShapes 函数接受一个 Shape 指针的向量,并遍历该向量调用每个对象的 draw() 函数。由于 draw() 是虚函数,所以实际调用的是每个具体对象的实现版本,这样就实现了多态。
通过多态,我们可以编写更加通用和灵活的代码。例如, drawShapes 函数可以接受任何继承自 Shape 的类的对象,使得函数的复用性大大增强。这种机制是OOP中实现可扩展性和可维护性的重要手段。
5. C++高级编程技巧
5.1 运算符重载与应用
运算符重载是C++提供的一种强大特性,它允许开发者自定义运算符的功能,适用于类类型的操作。这使得用户定义类型的使用更加直观和方便。
5.1.1 运算符重载的原理和限制
运算符重载本质上是函数重载的一种形式。在C++中,运算符重载是通过为类提供一个特殊的成员函数或友元函数来实现的,其名称是 operator 关键字后跟要重载的运算符符号。
运算符重载的限制包括: - 不能改变运算符的优先级。 - 不能创建新的运算符符号。 - 不能改变运算符的参数个数。 - 不能重载以下运算符: :: (域解析运算符)、 .* (成员指针访问运算符)、 ?: (条件运算符)、 sizeof (对象大小运算符)、 . (成员访问运算符)、 .* (成员指针访问运算符)。
5.1.2 实现自定义类型的运算符重载
下面是一个简单的例子,展示了如何为一个表示复数的类重载加法运算符( + )。
class Complex {
public:
double real, imag;
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// 加法运算符重载
Complex operator+(const Complex& rhs) const {
return Complex(real + rhs.real, imag + rhs.imag);
}
};
int main() {
Complex c1(1.0, 2.0);
Complex c2(2.0, 3.0);
Complex result = c1 + c2; // 使用重载的+运算符
// 输出结果
std::cout << "Result: " << result.real << " + " << result.imag << "i" << std::endl;
}
在这个例子中, Complex 类重载了加法运算符 + ,使得两个 Complex 对象的加法操作变得直观。注意,运算符重载函数的返回类型和参数类型可以根据需要自定义。
5.2 异常处理与标准库
异常处理是C++中的一个机制,用于处理程序运行时发生的错误和异常情况。它允许程序在检测到错误时优雅地退出,而不是让程序直接崩溃。
5.2.1 异常处理机制的理解与实现
异常处理主要涉及到三个关键字: try 、 catch 和 throw 。
-
try块:包围可能抛出异常的代码。 -
catch块:处理try块中的异常。 -
throw语句:抛出一个异常对象。
try {
// 可能抛出异常的代码
if (some_error_condition)
throw std::runtime_error("An error occurred");
} catch (const std::runtime_error& e) {
// 处理异常
std::cerr << "Caught exception: " << e.what() << std::endl;
}
异常处理机制允许程序在出现运行时错误时,能够执行一些清理工作并记录错误信息,同时提供了一种更加结构化的错误处理方式。
5.2.2 标准库中的容器、算法和迭代器
C++标准库提供了丰富的容器、算法和迭代器,它们为处理数据集合提供了一致而高效的接口。
- 容器 :如
std::vector、std::map、std::set等,用于存储数据。 - 算法 :如
std::sort、std::find、std::copy等,用于执行通用的算法操作。 - 迭代器 :提供一种方法访问容器中的元素,类似于指针。
容器、算法和迭代器的使用可以大幅减少代码量,提高可读性和效率。例如,对 std::vector 中的元素进行排序:
#include <vector>
#include <algorithm>
std::vector<int> vec = {3, 1, 4, 1, 5, 9};
std::sort(vec.begin(), vec.end()); // 使用标准算法sort进行排序
容器和算法的组合使用,可以让你编写非常复杂而强大的代码,而迭代器则成为了容器和算法之间的桥梁。
5.3 泛型编程与模板
模板是C++中的泛型编程的基础,它允许编写与数据类型无关的代码,从而实现类型安全的代码重用。
5.3.1 模板的基本概念和使用
模板分为函数模板和类模板。
- 函数模板 :可以用于创建通用的函数,操作不同类型的数据,而无需编写多个函数定义。
- 类模板 :可以用于创建可操作不同类型的通用数据结构,如通用容器。
例如,一个简单的函数模板:
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int max_int = max(10, 20);
double max_double = max(10.5, 20.3);
}
5.3.2 泛型编程在实际中的应用案例
泛型编程的一个经典例子是STL(标准模板库)。STL提供了许多通用的容器、算法和迭代器的实现,通过模板机制,它们可以用于任何数据类型。
例如,使用 std::sort 算法对自定义类型的数组进行排序:
struct MyStruct {
int value;
std::string name;
};
bool compareStructs(const MyStruct& a, const MyStruct& b) {
return a.value < b.value;
}
int main() {
std::vector<MyStruct> structs = {{1, "one"}, {3, "three"}, {2, "two"}};
std::sort(structs.begin(), structs.end(), compareStructs); // 使用自定义比较函数
}
模板和泛型编程使得代码更加灵活和通用,减少了代码的重复,提高了开发效率。
5.4 动态内存管理与智能指针
在C++中,动态内存管理是一个复杂但重要的主题。传统的 new 和 delete 操作符提供了手动内存管理的能力,但这可能导致内存泄漏和其他内存错误。智能指针是一种解决这些问题的现代C++特性。
5.4.1 new和delete的使用与陷阱
使用 new 关键字可以动态分配内存,并返回指向新分配对象的指针。使用 delete 可以释放之前通过 new 分配的内存。
int* p = new int(42); // 动态分配一个int对象并初始化为42
delete p; // 释放内存
然而,手动管理内存非常容易出错。如果不适当地使用 delete ,就会导致内存泄漏。如果对同一块内存使用了多次 delete ,则会产生未定义行为。
5.4.2 智能指针的理解与应用
为了避免手动管理内存的复杂性和潜在错误,C++提供了智能指针。智能指针在超出其作用域时,可以自动释放它们所拥有的资源。
最常用的智能指针类型包括: - std::unique_ptr :独占所拥有的资源。 - std::shared_ptr :允许多个指针共享同一个资源,资源会在最后一个拥有它的 shared_ptr 被销毁时释放。
下面是一个 std::unique_ptr 的示例:
#include <memory>
void processResource(std::unique_ptr<int> resource) {
// 使用resource处理资源
}
int main() {
std::unique_ptr<int> ptr = std::make_unique<int>(42); // 创建一个unique_ptr对象
processResource(std::move(ptr)); // 将资源传递给函数,转移所有权
// ptr不再拥有任何资源,如果尝试访问ptr将导致未定义行为
}
智能指针大大简化了内存管理,并增加了代码的健壮性。它们是现代C++开发中管理动态内存的推荐方法。
简介:《C++程序设计》是北京大学计算机系常宝宝教授编写的课件,旨在为C++初学者提供全面和深入的基础知识。本课程覆盖了C++的核心概念和技巧,包括基本语法、函数、指针、类与对象、构造与析构函数、运算符重载、动态内存管理、模板、异常处理和标准库使用等。同时,"数据结构(c++)"子文件深入讲解了C++在数据结构方面的应用,帮助学生掌握链表、栈、队列、树、图等数据结构的实现及优化。学习完成后,学生将能解决实际问题,并为深入学习计算机科学打下坚实基础。
1530

被折叠的 条评论
为什么被折叠?



