C++学习笔记之数组、指针和字符串

发布于:2025-07-09 ⋅ 阅读:(20) ⋅ 点赞:(0)

目录

一、数组的定义与使用

1、数组的概念

2、数组的定义

3、数组的使用

★重要知识点:在 C++ 中,数组索引从 0 开始,有效范围是 0 到 数组长度 - 1。访问超出此范围的索引(如 a[10] 对于长度为 10 的数组)属于越界操作,会导致未定义行为(如程序崩溃、数据错误或安全漏洞)。

二、数组的存储与初始化

1、一维数组的存储

2、一维数组的初始化

一维数组的应用举例:

3、二维数组的存储

4、二维数组的初始化

三、数组作为函数参数

四、对象数组

1、对象数组的定义与访问

2、对象数组初始化

3、数组元素所属类的构造函数

五、 指针

1、内存空间的访问方式

2、指针的概念

3、指针变量的定义

4、与地址相关的运算——“*”和“&”

六、指针变量的初始化和赋值

1、指针变量的初始化

2、指针变量的赋值运算

3、指针空值nullptr

4、指向常量的指针

5、指针类型的常量

6、指针的算数运算、关系运算

①指针类型的算术运算

②指针与整数相加的意义

③指针类型的关系运算

七、用指针访问数组元素

1、用指针访问数组元素

2、定义指向数组元素的指针

八、指针数组 

1、指针数组

2、指针数组与二维数组对比

3、不同数据类型的指针数组案例。

① 整数指针数组

②字符串指针数组(字符指针数组)

③函数指针数组

④动态分配的指针数组

九、以指针作为函数参数 

1、为什么需要用指针做参数?

2、指针类型的函数

3、指向函数的指针

十、动态分配与释放内存

1、动态申请内存操作符 new

2、释放内存操作符delete

3、分配和释放动态数组

十一、智能指针

1、智能指针

2、C++11 的智能指针


一、数组的定义与使用

1、数组的概念

        数组是具有一定顺序关系的若干相同类型变量的集合体,组成数组的变量称为该数组的元素。

2、数组的定义

        定义方式: 类型说明符  数组名[常量表达式]...;

        数组名的构成方法与一般变量名相同

  • 例如:int a[10];表示a为整型数组,有10个元素:a[0]…a[9]

  • 例如: int a[5][3];表示a为整型二维数组,其中第一维有5个下标(0~4),第二维有3个下标(0~2),数组的元素个数为15,可以用于存放5行3列的整型数据表格。

注意事项:

①数组定义的基本语法

        在大多数编程语言中,数组的定义需要明确数据类型和初始大小。例如在C语言中,定义数组时需要指定元素类型和数组长度:

int numbers[5]; // 定义一个包含5个整数的数组

②数组长度的确定

        数组长度可以是固定值或变量,但某些语言如C99之前的标准要求长度必须是常量表达式。现代语言如Python的列表或JavaScript的数组则更灵活:

my_list = [1, 2, 3]  # Python列表长度动态可变

③内存分配与初始化

        静态数组在编译时分配内存,动态数组在运行时分配。未初始化的数组可能包含垃圾值,建议显式初始化:

int[] arr = new int[3]{1, 2, 3}; // Java初始化

④多维数组的处理

        多维数组实际上是数组的数组。定义时需注意各维度的大小:

int[,] matrix = new int[2,3]; // C#二维数组

⑥边界检查

        数组访问越界是常见错误。某些语言如Java会自动检查,而C/C++不会:

int arr[3] = {0};
arr[3] = 1; // 未定义行为

⑦数据类型一致性

        数组元素通常要求同类型。若需混合类型,可使用结构体或对象数组:

let mixedArray = [1, "text", true]; // JS允许异质数组

⑧动态扩容机制

        了解语言的数组扩容策略很重要。例如C++的vector会预留额外容量,Python列表会动态调整大小。

3、数组的使用

使用数组元素必须先声明,后使用。只能逐个引用数组元素,而不能一次引用整个数组

  • 例如:a[0]=a[5]+a[7]-a[2*3]

  • 例如:b[1][2]=a[2][3]/2

数组的输出方式:
例:将数组 int a[4]={1,2,3,4}全部输出

for(int i=0;i<4;i++)
    cout << a[i];

 练习题
        定义int类型的两个数组ab,通过循环如示例输出一样打印反序的两个数组元素
示例输出:
a[0]=-1 b[0]=17

a[1]=1 b[1]=15

a[2]=3 b[2]=13

a[3]=5 b[3]=11

a[4]=7 b[4]=9

a[5]=9 b[5]=7

a[6]=11 b[6]=5

a[7]=13 b[7]=3

a[8]=15 b[8]=1

a[9]=17 b[9]=-1

示例代码:

#include <iostream>
using namespace std;

int main() {
    int a[10] = {-1, 1, 3, 5, 7, 9, 11, 13, 15, 17};
    int b[10];

    // 逆序赋值:a[0]->b[9], a[1]->b[8], ..., a[9]->b[0]
    for(int i = 0; i < 10; i++) {
        b[9 - i] = a[i]; // 用 9-i 作为 b 的索引
    }

    // 输出结果(注意索引改为 i)
    for(int i = 0; i < 10; i++) {
        cout << "a[" << i << "]=" << a[i] << " ";
        cout << "b[" << i << "]=" << b[i] << endl;
    }

    return 0;
}

★重要知识点:在 C++ 中,数组索引从 0 开始,有效范围是 0 到 数组长度 - 1。访问超出此范围的索引(如 a[10] 对于长度为 10 的数组)属于越界操作,会导致未定义行为(如程序崩溃、数据错误或安全漏洞)。

关键规则总结:

  1. 数组长度为 n 时,索引范围是 0 到 n-1
  2. 避免硬编码索引:使用循环变量(如 i < n)而非固定值(如 i <= 9)。
  3. 逆序赋值技巧:若要将 a 逆序存入 b,可使用 b[n-1-i] = a[i]

常见错误示例:

int a[10];
a[10] = 0;  // 错误:越界(有效索引为0~9)
for(int i=0; i<=10; i++) { ... }  // 错误:循环会执行到i=10

正确写法:

int a[10];
for(int i=0; i<10; i++) {  // 循环条件为 i < 10(即i最大为9)
    a[i] = ...;  // 安全访问
}
…………………………牢记这一点可以避免 90% 的数组相关 Bug!…………………………

二、数组的存储与初始化

1、一维数组的存储

  • 数组元素在内存中顺次存放,它们的地址是连续的。元素间物理地址上的相邻,对应着逻辑次序上的相邻

  • 例如:

2、一维数组的初始化

定义:在定义数组时给出数组元素的初始值。

  • 列出全部元素的初始值,例如:static int a[10]={0,1,2,3,4,5,6,7,8,9};

  • 可以只给一部分元素赋初值,例如:static int a[10]={0,1,2,3,4};

  • 在对全部数组元素赋初值时,可以不指定数组长度,例如:static int a[]={0,1,2,3,4,5,6,7,8,9}


一维数组的应用举例:

练习题1:

        循环从键盘读取若干组选择题答案,计算并输出每组答案的正确率,直到输入ctrl+z为止;每组连续输入五个答案,每个答案可以是'a'、'd'.
 

#include <iostream>
using namespace std;

int main() {
    // 定义正确答案数组(使用const确保不可修改)
    const char key[] = {'a', 'c', 'b', 'a', 'd'}; // 存储5个问题的正确答案
    const int NUM_QUES = 5;                      // 定义问题数量(使用const避免硬编码)
    
    char c;                          // 存储用户输入的当前字符
    int ques = 0;                    // 当前处理的问题编号(索引)
    int numCorrect = 0;              // 记录答对的题目数量
    
    cout << "Enter the " << NUM_QUES << " question tests:" << endl;

    // 主循环:逐字符读取用户输入(包括换行符)
    while(cin.get(c)) {              // cin.get(c)逐字符读取,包括空格和换行
        if(c != '\n') {              // 处理非换行字符(即用户输入的答案)
            if(c == key[ques]) {     // 答案正确
                numCorrect++;        // 正确计数器+1
                cout << " ";         // 输出空格表示正确
            } else {
                cout << "*";         // 输出星号表示错误
            }
            ques++;                  // 移动到下一个问题索引
            
            // 知识点:
            // 1. 数组索引从0开始,因此key[ques]对应第ques+1个问题的答案
            // 2. const数组确保答案不会被意外修改
            // 3. cin.get(c)与cin>>c的区别:前者读取所有字符(包括空格/换行),后者跳过空白字符
        }
        else {                       // 处理换行符(即用户完成一组输入)
            // 输出得分(使用类型转换确保浮点数除法)
            cout << " Score:" << static_cast<float>(numCorrect)/NUM_QUES*100 << "%";
            
            // 重置计数器,准备下一组输入
            ques = 0;                // 重置问题索引
            numCorrect = 0;          // 重置正确数量
            cout << endl;            // 换行
        }
        
        // 知识点:
        // 1. static_cast<float>(numCorrect):将整数转换为浮点数,避免整数除法截断
        // 2. 模块化思维:将处理逻辑分为"字符处理"和"换行处理"两个分支
        // 3. 状态重置:每次换行后需要重置ques和numCorrect
    }
    
    return 0;
}

核心代码思维解析:

  1. 输入处理

    • 使用 cin.get(c) 逐字符读取输入,包括换行符
    • 通过 c != '\n' 判断当前字符是答案还是换行符
  2. 状态管理

    • ques 作为数组索引跟踪当前问题编号(范围 0~4)
    • numCorrect 累计正确答案数量
    • 每次换行后重置状态变量
  3. 输出逻辑

    • 实时反馈:正确输出空格,错误输出星号
    • 换行时计算得分:使用 static_cast 确保浮点数除法

涉及的关键 C++ 知识点:

  1. 数组与索引

    • 数组索引从 0 开始
    • 使用 const 修饰常量数组
    • 越界访问风险(本例中 ques 由 0 递增到 4,未超过 key 数组长度)
  2. 输入输出

    • cin.get(c) 与 cin >> c 的区别
    • 格式化输出:cout << fixed << setprecision(2) 可控制小数位数
  3. 类型转换

    • static_cast<float>(int) 将整数转换为浮点数
    • 避免整数除法截断(如 5/5=1,而 5.0/5=1.0)
  4. 模块化设计

    • 使用 if-else 分支分离不同类型的输入处理
    • 状态变量的初始化和重置
  5. 常量定义

    • const int NUM_QUES 提高代码可维护性
    • 避免硬编码数字,便于后续修改问题数量

练习题2:

编程实现:编写函数silly,计算整数(不多于100位)犯二的程度并返回。

数字也可以“犯二”,一个整数“犯二的程度”定义为:

  • 该数字中包含2的个数与其位数的比值;
  • 如果这个整数是负数,则程度增加0.5倍;
  • 如果还是个偶数,则再增加1倍。

代码:

#include <iostream>
#include <cstring>
using namespace std;

double silly(char a[]);

int main() {
    char s[102];
    cin >> s;
    double rate = silly(s);
    // 取消注释以输出结果(题目中main函数未要求输出)
    // cout << "犯二率是" << rate << endl;
    return 0;
}

double silly(char a[]) {
    int count2 = 0;                // 计数器:2的个数
    int digits = 0;                // 有效数字位数(不含符号)
    bool isNegative = (a[0] == '-'); // 是否为负数
    int startIdx = isNegative ? 1 : 0; // 开始遍历的索引

    // 统计2的个数和有效位数
    for (int i = startIdx; a[i] != '\0'; i++) {
        digits++;
        if (a[i] == '2') count2++;
    }

    // 计算基础犯二率(避免整数除法)
    double rate = static_cast<double>(count2) / digits;

    // 应用修正系数
    if (isNegative) rate *= 1.5; // 负数修正

    // 末位是偶数则加倍(正确转换字符为数字)
    char lastChar = a[strlen(a) - 1];
    if ((lastChar - '0') % 2 == 0) rate *= 2.0;

    // 调试输出
    cout << "共有数字" << digits<<"个" << endl;
    cout << "其中共有数字“2”有" << count2<<"个" << endl;
    cout << "犯二率是" << rate << endl;

    return rate;
}

练习题3:

编程实现:编写一个函数king,实现猴子选大王的功能。

        新猴王的选择方法是:让N只预定猴子围成一圈(最多100只猴子),从某位置开始依次编号为1〜N号。从第1个号开始报数,每轮从1个报到3,凡报到3的猴子即退出圈子,接着又从相邻的下一只猴子开始同样的报数。如此不断循环,最后剩下的一只猴子就选为猴王。

代码:

#include <iostream>
using namespace std;

/**
 * 猴子选大王问题(约瑟夫环问题)
 * 参数:
 *   a[] - 猴子数组,a[i] 非零表示第i只猴子在圈中,0表示已出圈
 *   n   - 猴子总数(1-based索引)
 * 返回:
 *   最后剩下的猴子编号(1-based)
 */
int king(int a[], int n);

int main() {
    int n;
    cout << "请输入猴子数量(n>0):";
    cin >> n;

    // 初始化猴子数组:a[1]~a[n] 初始化为1~n,表示所有猴子在圈中
    int a[1000] = {0};
    for (int i = 1; i <= n; i++) {
        a[i] = i;
    }

    // 模拟出圈过程,返回最后剩下的猴子编号
    int kingIndex = king(a, n);
    cout << kingIndex << "号猴子是大王" << endl;
    
    return 0;
}

int king(int a[], int n) {
    int out = 0;      // 已出圈的猴子数量
    int k = 0;        // 当前报数计数(1-3循环)
    int next = 1;     // 当前检查的猴子索引(1-based)

    // 循环直到只剩1只猴子(需出圈n-1次)
    while (out < n - 1) {
        // 跳过已出圈的猴子,寻找下一个在圈中的猴子
        while (a[next] == 0) {
            if (++next > n) {
                next = 1;  // 环形结构:超出尾部则回到头部
            }
        }

        // 报数逻辑:每数到3时当前猴子出圈
        if (++k == 3) {
            a[next] = 0;  // 标记为出圈
            out++;        // 出圈数+1
            k = 0;        // 重置报数
        }

        // 移动到下一只猴子(环形结构)
        if (++next > n) {
            next = 1;
        }
    }

    // 遍历数组,找到唯一未出圈的猴子
    for (int i = 1; i <= n; i++) {
        if (a[i] != 0) {
            return i;  // 返回猴子编号(1-based)
        }
    }

    return 0;  // 理论上不会执行到这里
}


3、二维数组的存储

  • 按行存放,例如:float a[3][4];可以理解为:

  • 其中数组a的存储顺序为:a00 a01 a02 a03 a10 a11 a12 a13 a20 a21 a22 a23

4、二维数组的初始化

  • 将所有初值写在一个{}内,按顺序初始化,例如:static int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12

  • 分行列出二维数组元素的初值,例如:static int a[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};

  • 可以只对部分元素初始化,例如:static int a[3][4]={{1},{0,6},{0,0,11}};

  • 列出全部初始值时,第1维下标个数可以省略,例如:static int a[][4]={1,2,3,4,5,6,7,8,9,10,11,12};或:static int a[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};

  • 注意:

    • 如果不作任何初始化,内部auto型数组中会存在垃圾数据,static数组中的数据默认初始化为0;
    • 如果只对部分元素初始化,剩下的未显式初始化的元素,将自动被初始化为零;

三、数组作为函数参数

  • 数组元素作实参,与单个变量一样。

  • 数组名作参数,形、实参数都应是数组名(实质上是地址),类型要一样,传送的是数组首地址。对形参数组的改变会直接影响到实参数组。

示例:

编程实现:定义一个子函数rowSum,分别计算数组每一行的元素之和,然后在主函数中调用子函数rowSum输出各行元素的和。

#include <iostream>
using namespace std;

void rowSum(int a[][4], int b) {
    for(int i = 0; i < b; i++) {  // 从第0行开始,到b-1行结束
        for(int j = 1; j < 4; j++)  // 遍历当前行的每一列
            a[i][0] += a[i][j];  // 累加当前行的所有元素
    }
}


int main(){
    int table[3][4]={{1,2,3,4},{2,3,4,5},{3,4,5,6}};

    rowSum(table, 3); //调用子函数,计算各行和
    //输出计算结果
    for (int i = 0; i < 3; i++)
        cout << "Sum of row " << i+1 << " is " << table[i][0] << endl;
    return 0;
}

四、对象数组

1、对象数组的定义与访问

  • 定义对象数组:类名 数组名[元素个数]

  • 访问对象数组元素:通过下标访问数组名[下标].成员名

2、对象数组初始化

  • 数组中每一个元素对象被创建时,系统都会调用类构造函数初始化该对象。

  • 通过初始化列表赋值。例:Point a[2]={Point(1,2),Point(3,4)};

  • 如果没有为数组元素指定显式初始值,数组元素便使用默认值初始化(调用默认构造函数)。

3、数组元素所属类的构造函数

  • 元素所属的类不声明构造函数,则采用默认构造函数。

  • 各元素对象的初值要求为相同的值时,可以声明具有默认形参值的构造函数。

  • 各元素对象的初值要求为不同的值时,需要声明带形参的构造函数。

  • 当数组中每一个对象被删除时,系统都要调用一次析构函数。

五、 指针

1、内存空间的访问方式

  • 通过变量名访问

  • 通过地址访问

2、指针的概念

  • 指针:内存地址,用于间接访问内存单元

  • 指针变量:用于存放地址的变量

3、指针变量的定义

  • 例:static int i; static int* ptr = &i;
  • 例:*ptr = 3;

4、与地址相关的运算——“*”和“&”

  • 指针运算符:*

  • 地址运算符:&


六、指针变量的初始化和赋值

1、指针变量的初始化

  • 语法形式:存储类型 数据类型 *指针名=初始地址;

  • 例:int *pa = &a;·

  • 注意事项

    • 用变量地址作为初值时,该变量必须在指针初始化之前已声明过,且变量类型应与指针类型一致。
    • 可以用一个已有合法值的指针去初始化另一个指针变量。
    • 不要用一个内部非静态变量去初始化 static 指针。

2、指针变量的赋值运算

  • 语法形式:指针名=地址。注意:“地址”中存放的数据类型与指针类型必须相符

  • 向指针变量赋的值必须是地址常量或变量,不能是普通整数,例如:

    • 通过地址运算“&”求得已定义的变量和对象的起始地址
    • 动态内存分配成功时返回的地址
  • 例外:整数0可以赋给指针,表示空指针。

  • 允许定义或声明指向 void 类型的指针。该指针可以被赋予任何类型对象的地址。例:void *general;

3、指针空值nullptr

  • 以往用0或者NULL去表达空指针的问题:

    • C/C++ 的NULL宏是个被有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好。但是在C++的时代,这就会引发很多问题。
  • C++ 11使用nullptr关键字,是表达更准确,类型安全的空指针

4、指向常量的指针

  • 不能通过指向常量的指针改变所指对象的值,但指针本身可以改变,可以指向另外的对象。

int a;
const int *p1 = &a; //p1是指向常量的指针
int b;
p1 = &b; //正确,p1本身的值可以改变
*p1 = 1; //编译时出错,不能通过p1改变所指的对象

5、指针类型的常量

  • 若声明指针常量,则指针本身的值不能被改变。

int a;
int * const p2 = &a;
p2 = &b; //错误,p2是指针常量,值不能改变

6、指针的算数运算、关系运算

①指针类型的算术运算

  • 指针与整数的加减运算

  • 指针++,-- 运算

  • 指针p加上或减去n,其意义是指针当前指向位置的前方或后方第n个数据的起始位置。

  • 指针的++、-- 运算,意义是指向下一个或前一个完整数据的起始。

  • 运算的结果值取决于指针指向的数据类型,总是指向一个完整数据的起始位置。

  • 当指针指向连续存储的同类型数据时,指针与整数的加减运和自增自减算才有意义。

例子:

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // 让ptr指向数组的起始位置

    // 指针加上整数
    printf("ptr + 2 指向的元素值为: %d\n", *(ptr + 2));  // 输出: 30

    // 指针减去整数
    ptr = &arr[4];  // 使ptr指向最后一个元素
    printf("ptr - 3 指向的元素值为: %d\n", *(ptr - 3));  // 输出: 20

    // 指针递增
    ptr = arr;     // 重新让ptr指向数组起始位置
    ptr++;         // 此时ptr指向arr[1]
    printf("ptr 递增后指向的元素值为: %d\n", *ptr);  // 输出: 20

    // 指针递减
    ptr--;         // 此时ptr指向arr[0]
    printf("ptr 递减后指向的元素值为: %d\n", *ptr);  // 输出: 10

    // 指针相减(计算两个指针之间的元素个数)
    int *ptr1 = &arr[0];
    int *ptr2 = &arr[4];
    printf("ptr2 和 ptr1 之间相差的元素个数为: %td\n", ptr2 - ptr1);  // 输出: 4

    return 0;
}

②指针与整数相加的意义

③指针类型的关系运算

  • 指向相同类型数据的指针之间可以进行各种关系运算。

  • 指向不同数据类型的指针,以及指针与一般整数变量之间的关系运算是无意义的。

  • 指针可以和零之间进行等于或不等于的关系运算。例如:p==0或p!=0

示例:

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr1 = &arr[0];
    int *ptr2 = &arr[2];
    int *ptr3 = &arr[4];

    // 大于(>)运算
    if (ptr3 > ptr1) {
        printf("ptr3 的地址比 ptr1 大\n");  // 会执行此语句
    }

    // 小于(<)运算
    if (ptr1 < ptr2) {
        printf("ptr1 的地址比 ptr2 小\n");  // 会执行此语句
    }

    // 等于(==)运算
    if (ptr1 == &arr[0]) {
        printf("ptr1 指向数组的起始位置\n");  // 会执行此语句
    }

    // 大于等于(>=)运算
    ptr1 = ptr3;  // 让ptr1指向ptr3所指的位置
    if (ptr1 >= ptr3) {
        printf("ptr1 的地址大于或等于 ptr3\n");  // 会执行此语句
    }

    // 遍历数组(利用关系运算控制循环)
    printf("遍历数组的结果:");
    for (ptr1 = arr; ptr1 < arr + 5; ptr1++) {
        printf("%d ", *ptr1);  // 输出: 10 20 30 40 50
    }
    printf("\n");

    return 0;
}

七、用指针访问数组元素

1、用指针访问数组元素

  • 数组是一组连续存储的同类型数据,可以通过指针的算术运算,使指针依次指向数组的各个元素,进而可以遍历数组。

2、定义指向数组元素的指针

  • 定义与赋值,例:

    int a[10], *pa;
    pa=&a[0]; 或 pa=a;
    
  • 等效的形式

    • 经过上述定义及赋值后,*pa就是a[0]*(pa+1)就是a[1],… ,*(pa+i)就是a[i]a[i], *(pa+i), *(a+i), pa[i]都是等效的。
  • 注意:不能写 a++,因为a是数组首地址、是常量。

例子

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};  // 定义一个包含5个整数的数组
    int *ptr;                             // 声明一个指向整数的指针

    // 使指针指向数组的第一个元素(索引为0)
    ptr = &arr[0];
    
    // 也可以直接用数组名赋值,因为数组名会隐式转换为指向首元素的指针
    // ptr = arr;  

    // 输出指针指向的元素值
    printf("ptr 指向的元素值是: %d\n", *ptr);  // 输出: 10

    // 移动指针到数组的第三个元素(索引为2)
    ptr = &arr[2];
    printf("移动后 ptr 指向的元素值是: %d\n", *ptr);  // 输出: 30

    // 通过指针修改数组元素的值
    *ptr = 300;
    printf("修改后 arr[2] 的值是: %d\n", arr[2]);  // 输出: 300

    return 0;
}

八、指针数组 

1、指针数组

  • 数组的元素是指针型

2、指针数组与二维数组对比

①指针数组与二维数组的定义

  • 指针数组是一个数组,其元素均为指针。例如,int *arr[3]表示一个包含3个指针的数组,每个指针指向一个整数。
  • 二维数组是一个连续的内存块,按行和列组织数据。例如,int arr[3][4]表示3行4列的整数数组。

②内存布局差异

  • 指针数组中每个元素是一个独立的指针,可以指向不同长度的内存块。内存分配不连续,灵活性高。
  • 二维数组在内存中是连续存储的,所有元素按行优先或列优先顺序排列。固定大小,内存连续。

③访问方式对比

  • 指针数组通过双重解引用访问元素,例如*(arr[i] + j)arr[i][j]
  • 二维数组直接通过下标访问,例如arr[i][j],编译器自动计算偏移量。

④灵活性比较

  • 指针数组可以动态分配每一行的大小,适合不规则数据(如每行长度不同)。
  • 二维数组大小固定,必须在编译时确定,适合规则数据。

⑤性能考量

  • 二维数组由于内存连续,缓存命中率高,访问速度通常更快。
  • 指针数组可能因内存分散导致缓存效率低,但动态分配更灵活。

⑥适用场景

  • 指针数组适合需要动态调整行大小或处理不规则数据的场景。
  • 二维数组适合固定大小、需要高效访问的矩阵或表格数据。

代码示例

指针数组示例:

int *ptrArr[3];
for (int i = 0; i < 3; i++) {
    ptrArr[i] = malloc(4 * sizeof(int));
}
ptrArr[1][2] = 5; // 访问元素

二维数组示例:

int matrix[3][4];
matrix[1][2] = 5; // 访问元素

总结

指针数组提供动态灵活性,但牺牲部分性能;二维数组结构简单高效,但缺乏动态调整能力。选择取决于具体需求。
 

3、不同数据类型的指针数组案例。

① 整数指针数组
#include <stdio.h>

int main() {
    int a = 10, b = 20, c = 30;
    
    // 定义一个整数指针数组
    int *ptr_array[3];
    
    // 将指针指向各个整数变量
    ptr_array[0] = &a;
    ptr_array[1] = &b;
    ptr_array[2] = &c;
    
    // 通过指针数组访问值
    for(int i = 0; i < 3; i++) {
        printf("ptr_array[%d] = %p, *ptr_array[%d] = %d\n", 
               i, ptr_array[i], i, *ptr_array[i]);
    }
    
    return 0;
}
②字符串指针数组(字符指针数组)
#include <stdio.h>

int main() {
    // 定义一个字符指针数组,用于存储多个字符串
    const char *names[] = {
        "Alice",
        "Bob",
        "Charlie",
        "David",
        NULL  // 作为结束标记
    };
    
    // 遍历字符串数组
    for(int i = 0; names[i] != NULL; i++) {
        printf("Name %d: %s\n", i+1, names[i]);
    }
    
    return 0;
}
③函数指针数组
#include <stdio.h>

// 几个简单的函数
void sayHello() { printf("Hello!\n"); }
void sayGoodbye() { printf("Goodbye!\n"); }
void sayName() { printf("My name is Function.\n"); }

int main() {
    // 定义一个函数指针数组
    void (*func_ptr_array[])() = {sayHello, sayGoodbye, sayName};
    
    // 通过数组调用函数
    for(int i = 0; i < 3; i++) {
        printf("Calling function %d: ", i+1);
        func_ptr_array[i]();
    }
    
    return 0;
}
④动态分配的指针数组
#include <stdio.h>
#include <stdlib.h>

int main() {
    int size = 5;
    
    // 动态分配一个指针数组
    int **ptr_array = (int **)malloc(size * sizeof(int *));
    
    // 为每个指针分配内存并赋值
    for(int i = 0; i < size; i++) {
        ptr_array[i] = (int *)malloc(sizeof(int));
        *ptr_array[i] = i * 10;
    }
    
    // 打印值
    for(int i = 0; i < size; i++) {
        printf("ptr_array[%d] = %p, *ptr_array[%d] = %d\n", 
               i, ptr_array[i], i, *ptr_array[i]);
    }
    
    // 释放内存
    for(int i = 0; i < size; i++) {
        free(ptr_array[i]);
    }
    free(ptr_array);
    
    return 0;
}

九、以指针作为函数参数 

1、为什么需要用指针做参数?

  • 需要数据双向传递时(引用也可以达到此效果)

  • 用指针作为函数的参数,可以使被调函数通过形参指针存取主调函数中实参指针指向的数据,实现数据的双向传递

  • 需要传递一组数据,只传首地址运行效率比较高

  • 实参是数组名时形参可以是指针

2、指针类型的函数

指针函数的定义形式

  • 若函数的返回值是指针,该函数就是指针类型的函数。

  • 定义形式

存储类型 数据类型 *函数名()
{ //函数体语句
}
  • 注意

    • 不要将非静态局部地址用作函数的返回值。错误的例子:在子函数中定义局部变量后将其地址返回给主函数,就是非法地址
    • 返回的指针要确保在主调函数中是有效、合法的地址。正确的例子:主函数中定义的数组,在子函数中对该数组元素进行某种操作后,返回其中一个元素的地址,这就是合法有效的地址
    • 返回的指针要确保在主调函数中是有效、合法的地址。正确的例子:在子函数中通过动态内存分配new操作取得的内存地址返回给主函数是合法有效的,但是内存分配和释放不在同一级别,要注意不能忘记释放,避免内存泄漏

3、指向函数的指针

函数指针的定义

  • 定义形式:存储类型 数据类型 (*函数指针名)();

  • 含义:函数指针指向的是程序代码存储区。

函数指针的典型用途——实现函数回调

  • 通过函数指针调用的函数。例如将函数的指针作为参数传递给一个函数,使得在处理相似事件的时候可以灵活的使用不同的方法。

  • 调用者不关心谁是被调用者,需知道存在一个具有特定原型和限制条件的被调用函数。


十、动态分配与释放内存

1、动态申请内存操作符 new

  • new 类型名T(初始化参数列表)

  • 功能:在程序执行期间,申请用于存放T类型对象的内存空间,并依初值列表赋以初值。

  • 结果值:成功:T类型的指针,指向新分配的内存;失败:抛出异常。

2、释放内存操作符delete

  • delete 指针p

  • 功能:释放指针p所指向的内存。p必须是new操作的返回值。

3、分配和释放动态数组

  • 分配:new 类型名T [ 数组长度 ]

    • 数组长度可以是任何表达式,在运行时计算
  • 释放:delete[] 数组名p

    • 释放指针p所指向的数组。
    • p必须是用new分配得到的数组首地址。

例:为了动态声明一个内容为”string”的字符串

char *str=new char[10]; strcpy(str,"string");

十一、智能指针

1、智能指针

  • 显式管理内存在是能上有优势,但容易出错。

  • C++11提供智能指针的数据类型,对垃圾回收技术提供了一些支持,实现一定程度的内存管理

2、C++11 的智能指针

  • unique_ptr :不允许多个指针共享资源,可以用标准库中的move函数转移指针

  • shared_ptr :多个指针共享资源

  • weak_ptr :可复制shared_ptr,但其构造或者释放对资源不产生影响


十二、vector对象

1、为什么需要vector?

  • 封装任何类型的动态数组,自动创建和删除。

  • 数组下标越界检查。

  • ArrayOfPoints也提供了类似功能,但只适用于一种类型的数组。

2、vector对象的定义

  • vector<元素类型> 数组对象名(数组长度);

  • 例:vector<int> arr(5),建立大小为5的int数组

3、vector对象的使用

  • 对数组元素的引用

    • 与普通数组具有相同形式:vector对象名 [ 下标表达式 ]
  • vector数组对象名不表示数组首地址

    • 获得数组长度
    • 用size函数:数组对象名.size()

案例

使用 vector 存储和遍历学生成绩

以下是一个使用 std::vector 存储学生成绩并计算平均分的案例:

#include <iostream>
#include <vector>

int main() {
    // 创建 vector 存储成绩
    std::vector<int> scores = {85, 90, 78, 92, 88};

    // 计算总分
    int sum = 0;
    for (int score : scores) {
        sum += score;
    }

    // 计算并输出平均分
    double average = static_cast<double>(sum) / scores.size();
    std::cout << "平均分: " << average << std::endl;

    return 0;
}

使用 vector 动态添加数据

vector 可以动态调整大小,适用于运行时不确定数据量的场景:

#include <iostream>
#include <vector>

int main() {
    std::vector<std::string> names;

    // 动态添加数据
    names.push_back("Alice");
    names.push_back("Bob");
    names.push_back("Charlie");

    // 遍历输出
    for (const auto& name : names) {
        std::cout << name << std::endl;
    }

    return 0;
}

使用 vector 进行排序

可以利用 <algorithm> 库对 vector 进行排序:

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 3};

    // 升序排序
    std::sort(numbers.begin(), numbers.end());

    // 输出排序结果
    for (int num : numbers) {
        std::cout << num << " ";
    }

    return 0;
}

使用 vector 存储自定义对象

vector 可以存储自定义类或结构体:

#include <iostream>
#include <vector>

struct Point {
    int x;
    int y;
};

int main() {
    std::vector<Point> points = {{1, 2}, {3, 4}, {5, 6}};

    // 遍历输出点坐标
    for (const auto& p : points) {
        std::cout << "(" << p.x << ", " << p.y << ")" << std::endl;
    }

    return 0;
}

这些案例展示了 vector 在存储、遍历、动态调整和排序方面的灵活性,适用于多种编程场景。


十三、string类

1、string类

  • 使用字符串类string表示字符串

  • string实际上是对字符数组操作的封装

2、string类常用的构造函数

  • string(); //默认构造函数,建立一个长度为0的串。例:string s1;

  • string(const char *s); //用指针s所指向的字符串常量初始化string对象。例:string s2 = “abc”;

  • string(const string& rhs); //复制构造函数。例:string s3 = s2;

3、string类常用操作

  • s + t将串s和t连接成一个新串

  • s = t用t更新s

  • s == t判断s与t是否相等

  • s != t判断s与t是否不等

  • s < t判断s是否小于t(按字典顺序比较)

  • s <= t判断s是否小于或等于t (按字典顺序比较)

  • s > t判断s是否大于t (按字典顺序比较)

  • s >= t判断s是否大于或等于t (按字典顺序比较)

  • s[i]访问串中下标为i的字符

  • 例:

string s1 = "abc", s2 = "def";
string s3 = s1 + s2; //结果是"abcdef"
bool s4 = (s1 < s2); //结果是true
char s5 = s2[1]; //结果是'e'

4、考虑:如何输入整行字符串?

* 用cin的>>操作符输入字符串,会以空格作为分隔符,空格后的内容会在下一回输入时被读取
  • 输入整行字符串
    • getline可以输入整行字符串(要包string头文件),例如:getline(cin, s2);
    • 输入字符串时,可以使用其它分隔符作为字符串结束的标志(例如逗号、分号),将分隔符作为getline的第3个参数即可,例如:getline(cin, s2, ‘,’);

网站公告

今日签到

点亮在社区的每一天
去签到