本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C++是一种功能强大的编程语言,尤其受到专业人士的喜爱,因其灵活性、性能和面向对象的特性。本资源旨在为初学者提供C++基础知识的学习材料,包括基本语法、数据类型、控制结构、函数、类与对象、内存管理、标准库、模板和异常处理等方面。通过核心代码示例,学习者可以逐步掌握C++编程基础,并为深入学习C++的高级特性奠定基础。

1. C++基本语法介绍

1.1 C++程序结构

C++程序由一系列函数和变量组成,每个C++程序都必须包含一个名为 main 的函数,它是程序执行的入口点。C++中严格区分大小写,并且每条语句的结尾通常都需要一个分号 ;

#include <iostream>

int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

在上面的示例中, #include <iostream> 是一个预处理指令,它告诉编译器包含标准输入输出流库。 std::cout 是用来输出数据到控制台的流对象, << 是插入运算符, std::endl 是插入换行符并刷新输出缓冲区。

1.2 数据类型和变量

C++支持多种内置的数据类型,包括整型、浮点型、字符型等。变量是在程序中存储数据的命名位置。在声明变量时,必须指定其类型。

int integerVar = 10;   // 整型变量
double doubleVar = 3.14;  // 浮点型变量
char charVar = 'A';  // 字符型变量

1.3 运算符和表达式

运算符是用于执行数据运算的符号。C++中常用的运算符包括算术运算符、关系运算符、逻辑运算符等。表达式是由变量、常量和运算符组合而成的式子。

int a = 5, b = 3, sum;
sum = a + b;  // 算术表达式
if (sum > 8) {  // 关系表达式
    std::cout << "Sum is greater than 8." << std::endl;
}

在上面的代码中, sum = a + b; 就是一个赋值表达式,其中 = 是赋值运算符。而 if (sum > 8) 是一个条件表达式,其中 > 是关系运算符。

C++基本语法的介绍是学习这门语言的基石,它为后续章节中更复杂的概念和结构奠定了基础。

2. C++数据类型详解

2.1 基本数据类型

2.1.1 整型家族

在C++中,整型家族包含了多种数据类型,用于存储整数。包括有 int short long long long unsigned signed 等,每种类型都有其特定的存储范围。

int 类型为例,它通常至少能容纳一个机器字长的整数。但是,它的确切大小依赖于编译器和执行程序的机器架构,但通常情况下,一个 int 在32位系统上占用4个字节,而在64位系统上也占用4个字节。

#include <iostream>

int main() {
    int a = 10;
    short b = 20;
    long c = 30L;
    long long d = 40LL;
    std::cout << "int size: " << sizeof(a) << " bytes\n";
    std::cout << "short size: " << sizeof(b) << " bytes\n";
    std::cout << "long size: " << sizeof(c) << " bytes\n";
    std::cout << "long long size: " << sizeof(d) << " bytes\n";
    return 0;
}

在上面的代码示例中,我们分别定义了 int short long long long 类型变量,并使用 sizeof 运算符打印了它们的大小。这些信息有助于理解不同整型在内存中的占用大小。

2.1.2 浮点型家族

浮点型家族用于表示实数,主要包括 float double long double 三种类型。它们各自具有不同的精度和表示范围。

float 类型占用4个字节,通常精度为7位有效数字; double 类型占用8个字节,精度通常为15-16位有效数字;而 long double 的大小和精度则依赖于编译器和平台。

#include <iostream>
#include <limits> // For std::numeric_limits

int main() {
    float a;
    double b;
    long double c;

    std::cout << "float min: " << std::numeric_limits<float>::min() << "\n";
    std::cout << "double min: " << std::numeric_limits<double>::min() << "\n";
    std::cout << "long double min: " << std::numeric_limits<long double>::min() << "\n";
    return 0;
}

这段代码利用 <limits> 库中的 std::numeric_limits 模板类查询并打印了不同浮点类型能表示的最小值。它显示了每种浮点类型所能提供的精度和范围。

2.2 复合数据类型

2.2.1 数组

数组是一种数据结构,可以存储固定大小的相同类型元素。数组中的每个元素通过数组索引进行访问。

#include <iostream>

int main() {
    int arr[5] = {1, 2, 3, 4, 5}; // 定义并初始化一个整型数组
    for (int i = 0; i < 5; ++i) {
        std::cout << arr[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

上述代码定义了一个整型数组 arr ,并用循环遍历打印其元素。数组是连续内存分配的典型示例,且数组大小必须在编译时确定。

2.2.2 字符串

字符串在C++中可以表示为字符数组,并且C++标准库提供了字符串类 std::string ,更方便地处理字符串数据。

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, C++!";
    std::cout << "Length of str: " << str.length() << std::endl;

    return 0;
}

这里 std::string 对象 str 存储了字符串 "Hello, C++!" ,并且使用 length() 函数来获取字符串长度。 std::string 类提供了丰富的操作,包括连接、比较、查找等,远比C风格的字符串数组更易用和强大。

2.2.3 结构体和联合体

结构体和联合体是用户自定义的复合类型,它们可以将不同类型的数据组合到一起。

结构体使用关键字 struct 定义,它允许您创建包含不同数据类型的数据结构。

#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    Point p = {10, 20};
    std::cout << "Point coordinates: (" << p.x << ", " << p.y << ")" << std::endl;

    return 0;
}

这段代码定义了一个 Point 结构体来存储二维坐标,并创建了一个实例 p ,随后打印了其坐标值。结构体广泛用于封装相关数据。

联合体使用关键字 union 定义,它允许在相同的内存位置存储不同的数据类型,但一次只能存储其中一个成员的值。

#include <iostream>

union Number {
    int iValue;
    float fValue;
};

int main() {
    Number n;
    n.iValue = 123456; // 存储int类型值
    std::cout << "int value: " << n.iValue << std::endl;
    std::cout << "float value: " << n.fValue << std::endl; // 输出float值,结果依赖于内存中的内容

    return 0;
}

本段代码演示了联合体的定义和使用。由于联合体在内存中只占据一个成员的大小,因此 n.fValue 的输出依赖于 n.iValue 在内存中的表示。联合体适合用于节省内存或者类型转换的场景。

2.3 指针与引用

2.3.1 指针的基本概念

指针是一个变量,其值为另一个变量的地址。指针可以用来存储动态内存分配的地址,也可以用于函数参数传递时获取地址值。

#include <iostream>

int main() {
    int val = 10;
    int* ptr = &val; // 指针ptr存储了val的地址
    std::cout << "Value of val: " << val << std::endl;
    std::cout << "Value of val via ptr: " << *ptr << std::endl;
    return 0;
}

在这个例子中,我们声明了一个整型变量 val 和一个指向 int 的指针 ptr 。通过 ptr 我们能够间接访问 val 的值。指针广泛应用于多级间接访问、动态内存分配和函数参数传递。

2.3.2 指针与数组

在C++中,数组名可以被看作是一个指向数组首元素的常量指针。指针和数组可以一起使用,以访问数组元素。

#include <iostream>

int main() {
    int arr[3] = {10, 20, 30};
    int* ptr = arr; // 指针ptr指向数组arr的第一个元素
    for (int i = 0; i < 3; ++i) {
        std::cout << *(ptr + i) << " "; // 使用指针偏移访问数组元素
    }
    return 0;
}

在上面的代码中,指针 ptr 指向数组 arr 的第一个元素。通过指针算术,我们访问并打印了数组的每个元素。

2.3.3 引用的使用方法

引用是一种别名,它为已经存在的变量提供了一个新的名字。引用必须在定义时初始化,且之后不能改变其引用的对象。

#include <iostream>

int main() {
    int original = 20;
    int& ref = original; // ref成为original的引用
    std::cout << "Original: " << original << std::endl;
    std::cout << "Reference: " << ref << std::endl;

    ref = 40; // 通过引用修改原始值
    std::cout << "Modified Original: " << original << std::endl;
    std::cout << "Modified Reference: " << ref << std::endl;

    return 0;
}

在示例代码中,我们定义了一个 int 类型的变量 original 和它的引用 ref 。我们通过 ref 修改了 original 的值,展示了引用如何作为原变量的别名存在。由于引用总是指向同一个对象,因此可以提高程序的可读性和易用性。

3. C++控制结构应用

3.1 条件控制

在编程中,条件控制是一种基本的控制结构,用于根据条件表达式的真假来控制程序的执行流程。C++提供了多种条件控制语句,以实现程序的分支处理。接下来将详细探讨 if 语句和 switch 语句的用法和特点。

3.1.1 if语句

if 语句是C++中最基本的条件判断语句,允许根据给定的条件执行不同的代码分支。基本的 if 语句格式如下:

if (condition)
{
    // 条件为真时执行的代码
}

除此之外,C++还支持 if-else if-else if-else 结构,以处理多条件分支的情况。

if (condition1)
{
    // 条件1为真时执行的代码
}
else if (condition2)
{
    // 条件2为真时执行的代码
}
else
{
    // 前面的条件都不为真时执行的代码
}

在实际应用中, if 语句可以嵌套使用,以实现更复杂的逻辑判断。

3.1.2 switch语句

switch 语句是一种多分支选择结构,它根据变量的值选择执行不同的代码块。其基本语法如下:

switch (expression)
{
    case constant1:
        // 当expression等于constant1时执行的代码
        break;
    case constant2:
        // 当expression等于constant2时执行的代码
        break;
    // 可以有任意数量的case语句
    default:
        // 当没有case匹配时执行的代码
}

switch 语句在处理多个固定选项时比多个 if-else if 更清晰、更易于维护。需要注意的是, switch 语句中的每个 case 后面通常需要一个 break 语句,以防止执行完一个分支后继续执行下一个分支。

3.2 循环控制

循环控制结构用于多次执行一段代码直到满足特定条件。C++提供了几种循环语句: for 循环、 while 循环和 do-while 循环,以及用于跳出当前循环的 break 和跳过当前循环的 continue

3.2.1 for循环

for 循环是最常用的循环控制结构,特别是在已知循环次数的情况下。其基本语法如下:

for (initialization; condition; update)
{
    // 循环体中的代码
}
  • initialization :初始化部分,通常用于声明并初始化循环计数器。
  • condition :循环条件,必须为布尔表达式。
  • update :更新部分,在每次循环结束时执行,通常用于递增或递减循环计数器。

3.2.2 while与do-while循环

while 循环和 do-while 循环更适合在不确定循环次数的情况下使用。

while 循环的基本语法如下:

while (condition)
{
    // 循环体中的代码
}

do-while 循环则至少执行一次循环体,之后再检查循环条件是否满足,其语法为:

do
{
    // 循环体中的代码
} while (condition);

3.2.3 跳转语句的应用

在循环结构中, break 语句用于立即退出循环,而 continue 语句用于跳过当前循环的剩余部分,直接进入下一次循环的判断。

for (int i = 0; i < 10; ++i)
{
    if (i == 5)
    {
        break; // 当i等于5时退出循环
    }
    if (i % 2 == 0)
    {
        continue; // 当i为偶数时跳过当前循环
    }
    // 其他操作...
}

在嵌套循环中, break 只会影响最近的内层循环,而 goto 语句可以跳转到标签所在的位置,但通常不推荐使用 goto 因为它会使程序的流程难以理解。

在这一章节中,我们深入探讨了C++条件控制和循环控制的多种方式,以及它们在不同编程场景中的应用。理解并熟练运用这些控制结构对于编写高效且可维护的代码至关重要。接下来的章节将继续深入探讨C++函数的定义与使用,这是构建复杂程序不可或缺的部分。

4. C++函数定义与使用

4.1 函数基础

4.1.1 函数的定义

函数是C++程序中的基本构件,用于封装一组执行特定任务的语句。函数的定义告诉编译器函数的名称、返回类型、形参列表以及函数体。

一个基本的函数定义包括以下几个部分:

  • 返回类型(Return Type):指定函数执行完毕后返回的值的类型。
  • 函数名(Function Name):函数的唯一标识符,用来在程序中调用函数。
  • 参数列表(Parameter List):函数执行任务所需的数据,可以为空。
  • 函数体(Function Body):包含执行具体任务的代码。

函数定义的一般形式如下:

返回类型 函数名(参数列表)
{
    // 函数体
}

下面是一个简单的函数定义示例:

int add(int a, int b)
{
    int sum = a + b;
    return sum;
}

在这个例子中, add 函数接受两个整型参数 a b ,将它们相加后返回结果。

4.1.2 函数的声明与原型

函数声明(也称为函数原型)告诉编译器函数的名称、返回类型和参数类型,但不提供函数体。函数声明位于函数定义之前,使得编译器在遇到函数调用时能检查类型是否匹配,而不需要函数的全部实现细节。

函数声明的一般形式如下:

返回类型 函数名(参数类型1, 参数类型2, ...);

例如,对于 add 函数,其声明如下:

int add(int, int);

函数声明应包含足够的信息,以便编译器在编译时能够处理函数调用。函数声明通常放在头文件中,并在需要使用该函数的源文件中包含这个头文件。

4.2 函数高级特性

4.2.1 默认参数

在C++中,函数可以为参数提供默认值。如果函数调用时省略了具有默认值的参数,该参数将采用其默认值。默认参数增强了函数的灵活性,并减少了重载函数的需要。

定义带有默认参数的函数时,应确保默认参数在参数列表中靠后的位置,且所有默认参数必须位于非默认参数之后。

以下是一个默认参数示例:

void display(int a, int b = 10) // b具有默认值10
{
    // 函数体
}

在这个例子中,调用 display(5); 时,参数 b 将使用默认值 10

4.2.2 内联函数

内联函数是建议编译器在每个调用点直接插入函数代码的函数,以避免传统函数调用的开销。这通常用于短小的函数。

内联函数通过在函数定义前加上 inline 关键字来声明。编译器可能会忽略内联请求,因为它会考虑函数体的长度、函数使用的复杂性以及程序的大小等因素。

以下是一个内联函数的示例:

inline int square(int x)
{
    return x * x;
}

使用内联函数可以减少函数调用的开销,但是它会增加程序的大小。因此,内联函数适合用在那些函数调用次数频繁且执行时间短的场景。

4.2.3 函数重载

函数重载允许在同一个作用域内定义多个同名的函数,只要它们的参数列表不同。函数重载增加了代码的可读性和易用性。

编译器根据函数的参数类型、个数以及参数顺序来区分重载的函数。以下是一个函数重载的示例:

int sum(int a, int b)
{
    return a + b;
}

double sum(double a, double b)
{
    return a + b;
}

在这个例子中, sum 函数有两个版本,一个接受两个整数参数,另一个接受两个浮点数参数。根据传入参数的类型,编译器会决定调用哪个函数。

4.3 模板函数

4.3.1 函数模板的概念

函数模板是一种泛型编程工具,它允许编写与数据类型无关的代码。通过使用模板,可以为不同的数据类型创建相同的函数行为,而无需为每种类型编写重复的代码。

定义函数模板时,使用 template 关键字后跟模板参数列表。模板参数用尖括号 <> 包围,可以是类型参数(如 typename class ),也可以是非类型参数。

以下是一个函数模板的定义:

template <typename T>
T max(T a, T b)
{
    return (a > b) ? a : b;
}

在这个例子中, max 是一个函数模板,它接受两个类型为 T 的参数,并返回两者中的最大值。

4.3.2 模板函数实例化

当函数模板被调用时,编译器会根据提供的实参类型实例化出具体的函数版本,这一过程称为模板函数实例化。

模板实例化是自动进行的,开发者不需要手动编写代码来创建特定类型的函数副本。然而,开发者可以显式地控制实例化的类型,如下所示:

int main()
{
    int a = 5, b = 10;
    double c = 5.5, d = 10.2;

    // 自动实例化
    int m = max(a, b);       // 实例化为 max<int>(int, int)
    double n = max(c, d);    // 实例化为 max<double>(double, double)

    // 显式实例化
    int mi = max<int>(a, b); // 显式请求 int 类型的实例化版本

    // ...
}

4.3.3 模板特化与偏特化

模板特化允许为特定类型提供专门的模板实现,这在一些特定类型下需要不同处理逻辑时非常有用。

模板特化分为全特化(当为所有模板参数提供具体类型时)和偏特化(当只提供部分模板参数的具体类型时)。全特化可以看作是偏特化的特例。

全特化示例:

// 原始模板定义
template <typename T, typename U>
T max(T a, U b);

// 全特化版本
template <>
int max<int, int>(int a, int b)
{
    return (a > b) ? a : b;
}

偏特化示例:

// 原始模板定义
template <typename T, typename U>
T max(T a, U b);

// 偏特化版本,只特化了U类型
template <typename T>
T max(T a, T b)
{
    return (a > b) ? a : b;
}

在偏特化中,我们为U类型提供了一个默认类型,这样在调用时只需要提供T的类型即可。

模板特化为开发者提供了强大的工具来优化通用代码的性能,同时保持了代码的抽象性和复用性。

5. C++类与对象创建

5.1 面向对象基础

5.1.1 类和对象的定义

在C++中,类是一种用户定义的数据类型,可以包含数据成员(变量)和函数成员(方法)。类提供了一种将数据和操作数据的函数绑定在一起的机制。对象是类的实例,是根据类定义创建的变量。类定义了对象的布局和行为,而对象则是这些行为和数据的实际载体。

类定义

类定义的语法如下所示:

class ClassName {
private:
    // 私有成员,只能在类内部访问

public:
    // 公有成员,可以被外部访问和使用

protected:
    // 受保护成员,只能被类本身和派生类访问
};
对象创建

一旦定义了类,就可以创建其对象。对象的创建通常是在栈上完成的,但也可以通过动态内存分配在堆上创建。

ClassName objectName; // 在栈上创建对象
ClassName* ptr = new ClassName(); // 在堆上创建对象

5.1.2 构造函数与析构函数

构造函数和析构函数是类中的特殊函数,它们在对象创建和销毁时自动调用,用于初始化对象和清理资源。

构造函数

构造函数负责初始化类对象的数据成员,并进行必要的设置,如分配内存等。构造函数可以有参数,这允许在创建对象时进行定制化初始化。

class Rectangle {
private:
    int width, height;
public:
    Rectangle(int w, int h) : width(w), height(h) {} // 带参数的构造函数
};
析构函数

析构函数用于对象销毁前的清理工作,如释放分配的资源。析构函数没有参数,也不返回任何值,且类中只能有一个析构函数。

~Rectangle() {
    // 清理工作,比如释放内存
}

5.2 继承与多态

5.2.1 继承的实现

继承是面向对象编程的三大特性之一,它允许创建类的层次结构,其中新创建的类称为派生类(子类),而被继承的类称为基类(父类)。

派生类定义

派生类通过在其声明中添加一个冒号和访问说明符以及基类名称来继承基类。访问说明符可以是public、protected或private,它影响从基类继承的成员的可访问性。

class Base {};
class Derived : public Base {
    // 从Base类继承
};

5.2.2 虚函数与多态性

多态性允许通过派生类对象调用基类的函数,而执行的是派生类中的版本。实现多态的关键是将基类中的函数声明为虚函数。

虚函数声明

通过在函数声明前添加关键字 virtual ,可以将基类函数声明为虚函数。

class Base {
public:
    virtual void display() { // 虚函数
        // 默认行为
    }
};

class Derived : public Base {
public:
    void display() override { // 重写基类的虚函数
        // Derived类特有的行为
    }
};

5.2.3 抽象类和接口

抽象类是一种包含至少一个纯虚函数的类,纯虚函数是一种没有实现的虚函数,其声明以 = 0 结尾。抽象类不能直接实例化,它用作派生类的设计蓝图。

抽象类与接口

抽象类通常用于表示概念或行为,而接口则是完全由纯虚函数组成的类。

class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0; // 纯虚函数
};

class ConcreteClass : public AbstractClass {
public:
    void pureVirtualFunction() override { // 实现纯虚函数
        // 具体实现
    }
};

5.3 访问控制与封装

5.3.1 访问说明符

访问说明符 public protected private 控制着类成员的访问权限。这些访问说明符定义了类成员的保护级别。

访问说明符的作用
  • public 成员在类的外部可以被访问。
  • protected 成员在类和派生类的内部可以访问。
  • private 成员只能在类的内部访问。

5.3.2 友元函数和类

友元函数和友元类可以访问类的私有和受保护成员。友元不是类的成员函数,但需要在类声明中明确指定。

友元函数和类的声明

友元声明使用关键字 friend ,它允许某些外部函数或类访问类的私有成员。

class ClassA {
    friend void friendFunction(ClassA& a); // 声明友元函数
public:
    ClassA() : data(0) {}
private:
    int data;
};

// 定义友元函数
void friendFunction(ClassA& a) {
    a.data = 10; // 访问私有成员
}

5.3.3 封装的意义和实现

封装是面向对象编程的核心原则之一,它将数据和操作数据的方法捆绑在一起,并对外隐藏对象的实现细节。

封装的意义

封装确保对象的内部状态不被外部直接访问,而是通过一组公共成员函数来控制。这样可以保护对象不受外部修改,并允许内部实现细节的变化而不需要改变使用这些对象的代码。

封装的实现

封装通过将数据成员设置为私有( private )来实现,并通过公共接口(公有成员函数)来访问这些私有数据。

class Encapsulated {
private:
    int privateData; // 私有成员
public:
    void setPrivateData(int value) {
        privateData = value; // 公共成员函数用于访问和修改私有数据
    }

    int getPrivateData() const {
        return privateData; // 获取私有数据
    }
};

本章节对C++中类和对象创建的面向对象基础进行了深入的探讨,从类和对象的定义,到继承与多态的实现,再到封装与访问控制的细节,每一步都是对C++面向对象编程能力的进一步深化。理解并掌握这些基础概念对于后续章节中面向对象设计和高级特性的应用至关重要。

6. C++内存管理技巧

6.1 动态内存分配

6.1.1 new和delete运算符

在C++中,动态内存分配是通过 new delete 运算符来管理的。 new 运算符负责分配内存,并返回指向该内存的指针。 delete 运算符负责释放 new 运算符分配的内存。与C语言中的 malloc free 函数相比, new delete 不仅可以分配内存,还可以调用构造函数和析构函数。

int* ptr = new int; // 分配一个int类型的动态内存
*ptr = 5;           // 操作内存
delete ptr;         // 释放内存

在使用 new delete 时,要确保每个 new 都有一个匹配的 delete 来释放内存。未匹配的 new 会导致内存泄漏,而未匹配的 delete 可能导致程序崩溃。此外, new 可以用于分配数组,而 delete[] 用于释放数组内存。

6.1.2 指针数组与动态内存

指针数组是存储指针的数组,可用于动态管理多个对象的内存。当数组中的对象需要在运行时才能确定大小时,使用动态内存分配是一个好的选择。

int n;
cin >> n; // 假设从用户那里得到了数组大小
int* arr = new int[n]; // 分配数组内存

// 初始化数组
for (int i = 0; i < n; ++i) {
    arr[i] = i;
}

// 使用数组...

delete[] arr; // 释放数组内存

这段代码展示了如何使用 new 为一个整型数组动态分配内存,并使用 delete[] 释放内存。使用指针数组时,一定要注意防止数组越界和未初始化的使用。

6.2 智能指针

为了更安全地管理动态内存,C++标准库提供了智能指针。智能指针可以自动释放内存,从而避免内存泄漏。它们的行为类似于管理资源的类,可以确保资源被正确释放。

6.2.1 auto_ptr和unique_ptr

auto_ptr 是C++98中引入的,但在C++11中被 unique_ptr 所取代。 unique_ptr 提供了更严格的所有权语义,它不允许复制构造和赋值操作,防止了潜在的资源管理错误。

#include <memory>

std::unique_ptr<int> ptr = std::make_unique<int>(5); // 创建一个unique_ptr
*ptr = 10; // 操作对象

make_unique 是一个便捷的工厂函数,用于创建 unique_ptr 对象。与 new 直接分配不同, make_unique 提供了异常安全性,因为它会在构造过程中抛出异常时自动释放资源。

6.2.2 shared_ptr和weak_ptr

shared_ptr 用于多所有权管理,多个 shared_ptr 可以共享同一对象的所有权。当最后一个 shared_ptr 被销毁或重置时,对象被自动删除。

#include <memory>

std::shared_ptr<int> sp1 = std::make_shared<int>(5); // 创建一个shared_ptr
std::shared_ptr<int> sp2 = sp1; // sp2和sp1共享同一对象的所有权

为了打破 shared_ptr 可能造成的循环引用,C++11还引入了 weak_ptr weak_ptr 是一种不拥有对象的智能指针,它仅保留对 shared_ptr 管理的对象的引用。 weak_ptr 可以转换为 shared_ptr ,但它不会增加引用计数。

std::weak_ptr<int> wp = sp1; // 创建一个weak_ptr
auto sp3 = wp.lock(); // 尝试转换为shared_ptr

weak_ptr 适用于观察者模式和缓存实现,其中 shared_ptr 可能造成循环引用的情况。

6.3 内存泄漏的防范

6.3.1 内存泄漏的原因

内存泄漏是指程序在申请内存后未释放,导致内存无法被后续使用。这通常是由于指针变量的使用不当造成的,比如异常情况下未执行到释放内存的代码。在手动管理内存的程序中,内存泄漏是最常见的问题之一。

6.3.2 防范措施和检测方法

防范内存泄漏的最佳实践包括使用智能指针、遵循RAII(资源获取即初始化)原则和编写单元测试。

智能指针可以自动管理内存,减少手动分配和释放的错误。RAII原则指的是将资源封装到对象中,通过对象的构造函数和析构函数来管理资源的生命周期,这样可以确保资源被正确释放。

对于内存泄漏的检测,可以使用调试工具或运行时内存检测库,如Valgrind。此外,一些编译器和IDE提供了内存泄漏检测功能,可以集成到开发流程中。

graph LR
A[开始编程] --> B[使用智能指针]
B --> C[遵循RAII原则]
C --> D[编写单元测试]
D --> E[使用内存检测工具]
E --> F[避免内存泄漏]

通过这样的流程,我们可以在开发阶段就预防内存泄漏的发生,避免程序在运行时遇到内存不足的问题。

7. C++标准库功能概述

7.1 输入/输出库iostream

7.1.1 标准输入输出流

C++中的标准输入输出库iostream为程序员提供了一系列用于处理输入输出流的类和函数。它是C++标准库中不可或缺的一部分,通过这些类和函数,程序能够轻松地与控制台进行数据交换。

iostream类的层次结构

  • istream 类是用于输入操作的基类,它提供了读取数据的功能。
  • ostream 类是用于输出操作的基类,它提供了写入数据的功能。
  • iostream 类继承自 istream ostream ,因此它既可以读取也可以写入数据。
#include <iostream>

int main() {
    int n;
    std::cout << "Enter an integer: ";
    std::cin >> n;
    std::cout << "You entered " << n << std::endl;
    return 0;
}

在上面的代码示例中, std::cin 是一个 istream 对象,它用于从标准输入(通常是键盘)读取数据。 std::cout 是一个 ostream 对象,用于向标准输出(通常是控制台)写入数据。

7.1.2 文件流的使用

除了标准输入输出流外,iostream库还提供了对文件进行操作的能力。文件流类 ifstream 用于从文件读取数据,而 ofstream 用于向文件写入数据。

#include <fstream>
#include <iostream>

int main() {
    std::ifstream infile("input.txt");
    std::ofstream outfile("output.txt");

    if (!infile.is_open() || !outfile.is_open()) {
        std::cerr << "Error opening files." << std::endl;
        return -1;
    }

    std::string line;
    while (std::getline(infile, line)) {
        outfile << line << std::endl;
    }

    infile.close();
    outfile.close();
    return 0;
}

在上述代码中,我们打开了一个名为 input.txt 的文件用于读取,并打开一个名为 output.txt 的文件用于写入。通过 std::getline 函数逐行读取源文件的内容,并将其写入目标文件。使用 close() 函数确保在操作完成后关闭文件流,释放系统资源。

7.2 STL容器和算法

7.2.1 序列式容器

标准模板库(STL)提供了一系列序列式容器,如 vector deque list ,用于存储和管理数据序列。

向量(Vector)的使用

向量是一种动态数组,它可以存储一系列元素,这些元素可以是相同类型或通过存储自定义类型的对象。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec;
    vec.push_back(10);
    vec.push_back(20);
    vec.push_back(30);

    for (int i = 0; i < vec.size(); ++i) {
        std::cout << vec[i] << std::endl;
    }

    return 0;
}

在上面的代码中,我们创建了一个 vector<int> 类型的向量 vec ,并使用 push_back 方法添加了三个整数元素。然后,使用 size 方法获取向量中的元素数量,并遍历输出每个元素。

7.2.2 关联式容器

关联式容器如 set map 提供了键值对的存储功能,支持高效的数据检索和排序。

映射(Map)的使用

map 容器将键映射到值,可以快速检索和插入数据。

#include <iostream>
#include <map>

int main() {
    std::map<std::string, int> myMap;

    myMap["one"] = 1;
    myMap["two"] = 2;
    myMap["three"] = 3;

    for (auto& pair : myMap) {
        std::cout << pair.first << " => " << pair.second << std::endl;
    }

    return 0;
}

在这段代码中,我们创建了一个 map 容器 myMap ,它将 std::string 类型的键映射到 int 类型的值。我们添加了三个键值对,并通过范围for循环遍历 myMap 容器,输出了所有键和对应的值。

7.2.3 STL算法概述

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 num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这里,我们使用了 std::sort 算法对一个整数向量进行排序。 sort 函数接受两个迭代器参数,分别指向要排序的范围的开始和结束。

7.3 诊断和异常处理

7.3.1 异常类和异常机制

C++中的异常处理是一种处理程序运行时错误和异常情况的机制。异常类定义了一系列用于表示错误和异常状况的类。

基本的异常处理结构

#include <iostream>
#include <exception>

int main() {
    try {
        // 代码可能抛出异常的地方
        throw std::exception();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

在上面的示例中,我们使用 try 块包含了可能抛出异常的代码, catch 块用于捕获异常并执行相应的错误处理逻辑。

7.3.2 标准异常类的使用

C++标准库定义了几种标准异常类,如 std::logic_error std::runtime_error 等。这些异常类可用于处理更具体的异常情况。

#include <stdexcept>
#include <iostream>

int main() {
    try {
        throw std::invalid_argument("Invalid argument");
    } catch (const std::invalid_argument& e) {
        std::cerr << "Invalid argument exception: " << e.what() << std::endl;
    }

    return 0;
}

在上述代码中,我们抛出了一个 std::invalid_argument 异常。当这个异常被抛出时,相应的 catch 块会捕获它并输出错误信息。

7.3.3 自定义异常类

在实际的软件开发中,开发者可能需要根据具体的应用场景定义自己的异常类。

#include <iostream>

class MyException : public std::exception {
public:
    const char* what() const throw() {
        return "MyException occurred";
    }
};

void functionThatThrows() {
    throw MyException();
}

int main() {
    try {
        functionThatThrows();
    } catch (const MyException& e) {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

在这个例子中,我们定义了一个名为 MyException 的异常类,继承自 std::exception 。在 main 函数中调用可能抛出异常的函数,并捕获自定义异常,然后输出异常信息。

7.4 其他常用库

7.4.1 字符串库

C++提供了一个强大的字符串处理库 <string> ,可以用来创建和操作字符串对象。

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, C++!";
    std::cout << "The string is: " << str << std::endl;

    str += " Welcome to modern C++ programming.";
    std::cout << str << std::endl;

    return 0;
}

7.4.2 数值转换库

数值转换库 <numeric> 提供了一些实用的数值转换函数,例如 std::to_string 用于转换数值到字符串。

#include <iostream>
#include <string>
#include <numeric>

int main() {
    int num = 123;
    std::string str = std::to_string(num);
    std::cout << "The string is: " << str << std::endl;
    return 0;
}

7.4.3 时间日期库

C++11引入了时间日期库 <chrono> ,它提供了创建、操作和比较日期和时间的功能。

#include <iostream>
#include <chrono>
#include <thread>

int main() {
    using namespace std::chrono;
    steady_clock::time_point start = steady_clock::now();
    std::this_thread::sleep_for(seconds(1)); // Sleep for 1 second
    steady_clock::time_point end = steady_clock::now();

    auto duration = duration_cast<microseconds>(end - start);
    std::cout << "The operation took " << duration.count() << " microseconds." << std::endl;

    return 0;
}

在以上代码示例中,使用 std::this_thread::sleep_for 函数使当前线程暂停一秒,通过 steady_clock 来测量操作持续时间。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:C++是一种功能强大的编程语言,尤其受到专业人士的喜爱,因其灵活性、性能和面向对象的特性。本资源旨在为初学者提供C++基础知识的学习材料,包括基本语法、数据类型、控制结构、函数、类与对象、内存管理、标准库、模板和异常处理等方面。通过核心代码示例,学习者可以逐步掌握C++编程基础,并为深入学习C++的高级特性奠定基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐