指针与引用

I.指针

1.数据对象的地址与值

每一个变量,或者说数据对象,都有四个基本特征, VANT,一旦编译完成后, N(名称)T(类型)就没有了。实在上在程序代码里面,主要的就是地址和值

  • 地址: 数据对象的存储位置在计算机中的编号
  • 值: 在该位置处存储的内容

二者是辩证统一的关系,我们可以通过一个恰当的机制,让一个地址就是值或者一个值就是一个地址,而这个机制,就叫做指针

2.指针变量的定义和使用

指针的定义

  • 格式: 目标数据对象类型 * 指针变量名称;
  • 示例1: int * p; 定义 p 为指向整数的指针
  • 示例2: struct POINT {int x, y;}; POINT * p; 定义 p 为指向结构体类型的指针

多个指针变量的定义

  • 示例3: int * p, * q; p是一个指针,指向一个整数, q 也是一个指针, 指向一个整数, 不可以忘记 * 号
  • 示例4: typedef int * PINT; PINT p, q; 用typedef创建一个与原始类型一模一样的新类型,所以在程序中,所有出现 PINT 都可以使用 int *来代替,反之亦然; 这里定义了 p q 两个指向整数的指针
  • 示例5: typedef int * PINT; PINT p, * q; p是一个指向整数的指针, 而 q 这里是指向 PINT 的一个指针,即 q 是一个指向指针的指针

3.指针变量的存储布局

在使用指针的时候,指针数据对象事实上涉及到了两个数据对象,一个是指针数据对象本身, 第二个是指针所指向的目标数据对象

指针数据对象(变量)与目标数据对象(变量)仅定义指针变量,未初始化

  • 示例1: int * p;
  • 这里只是定义了一个指向整数的指针,未初始化, 也没有赋值,就只会分配一个指针变量的地址空间, 但是里面什么都没有

定义指针变量, 并使其指向某个目标变量

  • 示例2: int n = 10; int * p = &n;
  • 定义的时候同时对指针进行初始化,这时候就会同时涉及到两个数据对象, 在这里定义了一个整数 n, 值为 10, 定义了一个指针,初始化为 n 这个整数的基地址,把 n 的地址传给p; n 会被存放在内存中某个位置,假设这个地址的位置编号为 0x00130000, 里面存的数据为10, 那么 p 初始化的时候,将会保存 n 的基地址,即 p 的值就是 0x00130000。计算机架构有一个间接寻址机制,我们可以使用间接访问机制通过 p 来访问 p 所指向的目标数据 n, 为什么说是指向呢? 因为 n 存放在内存中的某个地方,n 的内容是10, 地址空间是 0x00130000, 而 p 里存放的就是 n 的地址,所以这里构造了一个 p 和变量 n 两者之间的指向关系。

定义指针变量,并使其指向数组首元素

  • 示例3: int a[8] = {1, 2, 3, 4, 5, 6, 7, 8}; int * p = a;
  • 定义了一个整数数组 a, 指向整数的指针 p, 把 p 初始化为这个数组 a 的基地址(即起始位置, a[0]的地址); 因为 a 是一个数组的名字, 所以之前加不加 & 操作符都可以。其实这里是构造了一个指针 p 指向了数组 a 中 0 号元 a[0] 的指向关系, p 作为一个指针,并不指向这个数组,而是仅仅指向 a[0]

4.指针变量的赋值

  • 示例: int n = 10; int * p = &n, * q; q = p;
  • 定义了一个整数 n,值为10, 定义了一个指针 p 指向 n, p 的值为 n 的基地址, 又定义了一个指针 q, 把 p 赋值给 q, 这样,就定义了两个同时指向同一个数据对象 n 的指针, p 和 q 这里是一样的。

5.取址操作符 &

  • 获取数据对象的地址, 可以将结果赋值给指针变量; 就是说,把这个地址作为值, 赋值给另外一个指针变量
  • 示例: int n = 10; int * p; p = &n; int * q; q = p;
  • 把int n 初始化为 10, 定义一个指针 p, 把变量 n 的地址赋值给 p, 那么 p 就是一个指向 n 的指针; 然后定义一个指针 q, 把指针 p 赋值给 q 或者把 &n 赋值给 q, 那么得到的结果也一样,就是得到了 p 和 q 同时指向整数 n 的两个指针

6.引领操作符

我们想用指针来访问指向的目标数据对象的话,就必须使用引领操作符

  • 使用引领操作符获取指针所指向的目标数据对象
  • 示例1:int m, n = 10; int * q = &n; m = *q;
  • q是指向整数 n 的指针,值为 n 的地址,通过 ```m = *q``使得 m = 10; 用 *q 去引领 q 所指向的目标数据对象; q 指向 n, 那么 *p 就代表着这个指针 p 所指向的目标数据对象 n, *p 的内容就是 10, 就可以把 n 的内容写到 m里。 换句话说, *p 就是 p 这个指针所指向的数据对象的值, *引领操作符,就是找到当前这个指针中保存的地址所对应的值, 而 &取址操作符就是获取这个数据对象值所在内存空间的地址

7.代码示例

编写程序,使用指针互换两个整数的值

#include <iostream>
using namespace std;

int main()
{
    int m = 10, n = 20, t;
    int * p = &m;
    int * q = &n;
    cout << "m: " << m << "; n: " << n <<";\n";
    t = *p;
    *p = *q;
    *q = t;
    cout << "m: " << m << "; n: " << n <<";\n";
    return 0;

//
}
// 虽然 m 和 n 的值变了,但是 p 和 q 的指向关系并没有改变, p一直指向 m, q 一直指向 n

8.指针的意义和作用

  • 作为函数通信的一种手段:使用指针作为函数参数,不仅可以提高参数的传递效率,还可以将参数作为函数输出集的一员,带回结果
  • 作为构造复杂数据结构的手段:使用指针构造数据对象之间的关联,形成复杂数据结构
  • 作为动态内存分配和管理的手段: 可以在程序执行期间动态构造数据对象之间的关联
  • 作为执行特定程序代码的手段: 使用指针指向特定代码段,执行未来才能实现的函数

9.指针与函数

数据交换函数

编写程序互换两个整型数据对象的值,要求使用函数实现数据对象值的互换

#include <iostream>
using namespace std;
void Swap(int * x, int * y);
int main()
{
    int m = 10, n = 20;
    // 条件编译指令
    // 宏测试:#ifndef NDEBUG
    #ifndef NDEBUG
        cout << "main (before swapped): m =" << m << "; n =" << n << endl;
    #endif
    // 调用Swap函数互换目标数据对象的值
    Swap(&m, &n);
    #ifndef NDEBUG
        cout << "main (after swapped): m =" << m << "; n=" << n <<endl;
    #endif
    return 0;

}

void Swap(int * x, int * y)
{
    int t;
    //判断数据有效性
    if(!x || !y)
    {
        cout << "Swap: Parameter(s) illegal." << endl;
        exit(1);
    }
    #ifndef NDEBUG
        cout << "Swap(before swapped): x = " << x << "; y = " << y << endl;
        cout << "Swap(before swapped): *x = " << *x << "; *y =" << *y << endl;
    #endif
    t = *x;
    *x = *y;
    *y = t;
    #ifndef NDEBUG
        cout << "Swap(after swapped): x = " << x << "; y = " << y << endl;
        cout << "Swap(after swapped): *x = " << *x << "; *y = " << *y << endl;
    #endif

}

条件编译指令:作为一个宏测试,#ifndef NDEBUG#endif 将 cout 封装了起来,意思就是如果没有定义这个宏NDEBUG,程序就会编译这段封装起来的代码,#endif就结束了这个判断;真实编程的时候,后面又不想要这段代码在程序里执行,不用删除,那么,就可以在程序开头,定义一个宏: #define NDEBUG, 这样,在程序中,#ifndef就不会通过,这段代码也不会被执行。 ps:可以在调试代码输出的时候,使用这种宏测试

在函数的栈框架中,运行swap的时候, swap()是覆盖 main()的栈框架的,但是,在swap(int * x, int * y)函数中,只能通过指针来访问 m 和 n 的,是不可以直接访问 m 和 n的;通过指针的取值操作符和引领操作符,可以访问指针所指地址所在的变量位置,进而修改值; 这里 int * x 就是 mint * y就是 n;

常量指针与指针常量

  • 常量指针: 指向常量的指针 - 性质: 不能通过指针修改目标数据对象的值(不可以使用引领操作符), 但是可以改变指针的值, 使其指向其他地方 - 示例一: int n = 10; const int * p = &n; 这里可以初始化, 也可以不初始化后面再赋值 - 典型使用场合: 作为函数的参数,表示函数内部不能修改指针所指向的目标数据对象值,即不能通过引领操作符进行修改目标数据对象的值 - 示例二: void PrintObject(const int * p); 输出目标数据对象的值和它的内容,并不会改变它的值,这时候,形式参数应该写 const int * p,而不是 int * p;因为写成 int * p, 就是以为函数在内部通过 *p修改目标数据对象的值 - 在写函数原型的时候,就应该把不通过引领操作符进行修改目标数据对象值的限定, 用常量指针明确的表示出来
  • 指针常量:指针指向的位置不可变化 - 性质: 不可将指针指向其他地方,但是可以改变指针所指向的目标数据对象的值 - 示例一:int n = 10; int * const p = &n; 这个时候,必须要对指针进行初始化,把 n 的地址通过 &n 传给它,因为 p 是一个常量,不可以被赋值 - 指针常量和其他常量一样,必须在定义的时候就进行初始化,不可以再被赋值 - 可以通过 *p去修改 n 的值
  • 常量指针常量:指向常量的指针常量(指针的双重只读属性) - 性质:指针值不可改变,指向的目标数据对象值也不可改变,即指针和指针指向的目标量都是常量 - 示例一: const int n = 10; const int * const p = &n; 这里,*pp都不可以被赋值 - 典型使用场合:主要作为函数参数的使用

指针与函数返回值

  • 指针类型可以作为函数的返回值 - 函数内部返回某个数据对象的地址 - 调用函数后将返回值赋值给某个指针 - 特别说明:不能返回函数内部定义的局部变量地址,只能返回某个全局量的地址,或者,返回作为函数的参数传给函数的指针 - 示例:
int global = 0;
int * ReturnPointer()
{
    return &global;
}

10指针与复合数据类型

指针与数组

数据对象地址的计算
  • 数组定义: int a[8] = {1, 2, 3, 4, 5, 6, 7, 8};
  • 数组的基地址: &aa
  • 数组元素地址: 数组的基地址就是数组0号元的基地址 - 数组首元素地址: &a[0] - 数组第 i 号元的地址: &a[0] + i*sizeof(int) - 数组的每一个元素是一个整数,每一个整数占用的大小是 sizeof(int), 且数组基地址与首元素地址数值相同,所以数组第 i 号元素地址就是上述表达式
  • 指针定义: - int * p; p = &a[0]; p 是指向数组的首元素的 - int * p; p = &a[0]; p 是指向数组的首元素的 - int * q; q = &a[2]; q 是指向 a[2]的
指针运算:
  • 希望表达 p、q 之间的联系:它们都指向同一数组中的元素
  • 指针与整数加减法运算 - 设 p 为指向整数数组种某元素的指针, i 为整数, 则 p + i 表示指针向后滑动 i 个整数, p - i表示向前滑动 i 个整数 - 示例一: p 指向 a[0],则 p + 2指向 a[2] - 示例二: p 指向 a[3], 则 p -2指向 a[1] - 指针与整数加减法运算的结果仍然为指针类型量,故可以赋值 - 示例三: p 指向 a[3], 则 q = p + 2;使得 q 指向 a[2]
  • 指针与整数加减运算规律 - 以指针指向的目标数据对象类型为单位, 而不是以字节为单位
  • 指针的递增递减运算 - 示例四:p 指向 a[0], 则 p++指向 a[1] - 示例五:p 指向 a[1], 则 --p指向 a[0]
  • 指针减法运算 - 两个指针的减法运算结果为其间元素个数,减法的结果就是一个整数,表示两个指针之间间隔的元素个数 - 示例六:p 指向 a[0], q 指向 a[2], q - p 结果为2
  • 指针关系运算 - 测试两个指针是否相等 (p == q;p != q;) - 示例七: 设 p、q为指针,则 p == q 测试两个指针是否指向同一个目标数据对象
空指针: NULL,是一个宏
  • 指针值 0: 表示指针不指向任何地方,表示为 NULL
  • 指向内存条最开头的存储区,现在主流的操作系统,在那个存储区是什么数据都不保存的,就只是用来捕获指针错误的
  • 示例八: 设 p 为指针, 则 p = NULL表示 p 不指向任何目标数据对象
  • 示例九: (测试指针 p 是否有意义): if(p != NULL) 等价于 if(p)
  • 在程序中,我们要保证指针 p, 要么是指向一个合法有效的数据对象,要么就是指向 0; 如果不测试指针是否有意义,那么在带指针的程序中,是非常容易出错的;如果一个指针在程序中指向了无意义的地方,或者无权访问的地方,那么程序就会崩溃;所以,在使用指针前,一定要测试其是否有意义
  • 如果不对指针初始化,那么指针就是个全局量,指向0; 如果这个指针为局部量,那么它内部的位序列就是随机的,即有可能不是0,那么一访问,目标数据对象区域没有权利访问的
作为函数参数的指针与数组
  • 数组作为函数参数:函数定义
void GenerateIntegers(int a[], unsigned int n)
{
    unsigned int i;
    Randomize();
    for(i = 0; i < n; i++)
        a[i] = GenerateRandomNumber(lower_bound, upper_bound);
}
数组作为函数参数,传入数组,和传入数组元素的个数,是能够带回来结果的
  • 数组作为函数参数:函数调用
#define NUM_OF_ELEMENTS 8
int a[NUM_OF_ELEMENTS];
GenerateIntegers(a, NUM_OF_ELEMENTS);

第一个函数传数组的基地址,第二个函数传数组的元素个数

  • 指针作为函数参数:函数定义

void GenerateIntegers(int * p, unsigned int n)
{
    unsigned int i;
    Randomize();
    for(i = 0; i < n; i++)
        *p++ = GenerateRandomNumber(lower_bound, upper_bound);
}

第一个参数 p 是指向整数的一个指针,第二个参数是元素的个数,我们内部访问它,就是用 *p(引领操作符), 来访问它所指向的数组 0 号元, 这里 *p++,是两个操作符,先把生成的数赋值给 *p,然后执行 p++, 移动指针到数组的下一个元素; 这个和 (*p)++不一样,(*p)++是将 p 指向目标数据对象的值进行 ++ 操作

  • 指针作为函数参数:函数调用
#define NUM_OF_ELEMENTS 8
int a[NUM_OF_ELEMENTS];
GenerateIntegers(a, NUM_OF_ELEMENTS);

和数组作为函数参数进行函数调用是一样的,但是要记住,必须传递已经分配空间的数组的基地址,不能传其他没有分配空间的指针

指针与数组的可互换性
  • 互换情况:指针一旦指向数组的基地址,则使用指针和数组格式访问元素时的地址计算方式是相同的,此时可以互换指针与数组操作格式
  • 程序示例:
int a[3] = {1, 2, 3};
int * p = &a;
int i;
for(i = 0; i < 3; i++)
{
    //正确,可以将指针 p 当成数组来处理
    //注意,这里仅仅是 p 和 a 的计算方式是一样的,两者并不等价
    //因为 p 仅仅指向 a 的零号元
    cout << p[i] << endl;
}

for(i = 0; i < 3; i++)
{
    //正确,可以将数组 a 当作指针来处理
    //这里,a + i 代表的是 a + i * sizeof(int)
    cout << *(a + i) << endl;
}

上述代码一般只在一维数组上有效,在高维数组上,就不一定等效了。

  • 例外情况:数组名为常数,不能在数组格式上进行指针运算
  • 程序示例:
//正确,指针 p 可以赋值, 指向下一个元素
// p 可以做 p++ 赋值,等价于 p = p + 1,
//即指向下一个元素
for(int i = 0; i < 3; i++)
    cout << *p++ << endl;

//错误,不能将数组 a 当作指针进行赋值
// a 作为一个数组的名字,代表数组 a 的基地址
// 所以 a 是不能做 a++ 运算的
for(int i = 0; i < 3; i++)
    cout << *a++ << endl;
指针与数组的差异
  • 使用指针或数组声明的数据对象性质不同 - 示例: int a[3] = {1, 2, 3}; int * p = &n; - 定义数组的同时,确定啦数组元素的存储布局: a 为静态分配内存的数组; 若 a 为全局数组,则程序执行前分配内存; 若为局部数组,则在进入该块时分配内存; int a[3]内部分配了 4 * 3 = 12 个字节(32位系统,32位编译器) - 定义指针的时候,规定指针数据对象的存储布局:p 为指针,若 p 为全局变量,则程序执行前,分配内存; 若为局部变量,则在进入该块时分配内存; int * p 则在内存中为 p 分配一个大小为 4个字节的空间 - 定义指针时,未规定目标数据对象的存储布局: p 为指针,指向一个已存在的数组的基地址,即指向该位置处的整数 a[0]; 若 p 未初始化, 则目标数据对象未知
  • 使用指针时,要显式地构造指针与目标对象的关联
多维数组作为函数参数
  • 函数原型: 正确示例(有不妥,非错误) - void PrintTwoDimensionalArray(int a[8][8], unsigned int m, unsigned int n); - 直接传递元素个数不妥当,只能处理固定元素个数的数组,应用场合非常受限,因为封装了两个魔数 8 8, 所以传递两个量 m n 进去,但是还是不妥,为什么,因为两个魔数并没有取消掉,这么写是正确的,但是还是不妥当
  • 函数原型: 错误示例 - void PrintTwoDimensionnalArray(int a[][], unsigned int m, unsigned int n); - 不能每一维都不传递元素的个数,语法规则不允许,语法规则要求两个中括号里头只能有一个可以不写
  • 函数原型: 正确示例(有不妥,非错误) - void PrintTwoDimensionalArray(int * a, unsigned int m, unsigned int n); - a 为指向数组基地址的整数指针, m 为第一维元素,n 为第二维元素个数,函数内部使用指针运算访问某个元素 - 如: 第 i 行,第 j 列元素, 使用指针运算 a + n * i + j 的结果指针指向 - 这样也不是很妥当,因为 a 本来是一个二维数组,却要当成一维数组来计算,也不合适,虽然正确,但是很复杂
  • C / C++ 里并没有一个好的方案来处理多维函数作为函数参数,建议用第一种方法写
  • 程序示例
//函数定义
void PrintTwoDimensionalArray(int * a, unsigned int m, unsigned int n)
{
    unsigned int i, j;
    for(i = 0; i < m; i++)
        for(j = 0; j < n; j++)
            cout << *(a + n*i + j) << ";";
}

//函数调用
int a[2][3] = {[1, 2, 3], [4, 5, 6]};
// 这里 [] 应该是 {},
PrintTwoDimensionalArray(a, 2, 3);

指针与结构体

指向结构体的指针对象
  • 指向结构体的只针对象声明
struct STUDENT
{
    int id;
    String name;
    int age;
};

STUDENT student = {2007010357, "Name", 19};
STUDENT * pstudent = &student;
  • 访问指针所指向的结构体对象的成员
(*pstudent).id = 2007010357;
(*pstudent).name = DuplicateString("Name");
(*pstudent).age = 19;

必须使用括号:选员操作符优先级高于引领操作符

  • 选员操作符 ->

pstudent->id = 2007010357;
pstudent->name = DuplicatingString("name");
pstudent->age = 19;

更方便,不用写括号

结构体成员类型为指针
  • 结构体成员类型为指针的声明
struct ARRAY
{
    unsigned int count;
    int * elements;
}
int a[8] = {1,2,3,4,5,6,7,8};
ARRAY array = { 8, &a}
  • 访问指针类型的结构体成员 - 访问 elements 的第 i 个元素: array.element[i] - 若有定义:ARRAY * parray = &array; - 访问 parray 指向的结构体对象 elements 的第 i 个元素: (*parray).elements[i]parray->elements[i]
结构体指针的使用场合
  • 使用指向结构体对象的指针作为函数参数 - 好处一: 节省结构体整体赋值的时间成本,传结构体地址比结构体整体赋值要快的多 - 好处二:解决普通函数参数不能直接带回结果的问题,可以在函数内部改变目标结构体对象的值
  • 构造复杂的数据结构 - 动态的创建和管理这些复杂的数据结构 - 动态数组:struct ARRAY{ unsigned int count; int * elements;};

II.字符串

1.字符串的表示

作为字符数组

  • 字符数组定义 - 与普通数组定义的格式相同 - 示例: char s[8] = {'C', 'P', 'P', '-', 'P', 'r', 'o', 'g'}; - 存储布局与普通数组相同
  • 字符数组的访问 - 按照数组的格式进行,逐一的访问每个元素,不方便
  • 多个字符数组连续存储时的问题 - 如何区分存储空间刚好连续的多个字符数组? - 示例:
char s[8] = {'C', 'P', 'P', '-', 'P', 'r', 'o', 'g'};
char t[5] = {'H', 'e', 'l', 'l', 'o'};

这两个数组的存储空间连在一起,’C’ 就在 ‘o’ 的后面, 无法区分,可能是一个字符串 HelloCPP-Prog, 可能是两个,或者好几个,区分不出来

-   解决方案:在字符数组末尾添加结束标志 ```'\0'```
-   示例:
char s[9] = {'C', 'P', 'P', '-', 'P', 'r', 'o', 'g', '\0'};
char t[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
-   优点:在程序运行时,通过测试 ```'\0'``` 字符确定字符数组是否结束,而不需要了解数组元素个数,使处理元素个数未知的数组成为可能通过指针运算直接操作字符数组种的字符,而不再使用数组格式访问字符
-   要记住每次需要给 ```'\0'```分配额外的内存空间 -    字符数组的访问
//编写函数,返回字符 c 在字符串 s 中的首次出现位置
unsigned int FindCharFirst(char c, char s[])
{
    unsigned int i;
    if (!s) {
        cout << "FindCharFirst: Illegal string.\n";
        exit(1);
    }

    for (int i = 0; s[i] != '\0'; i++) {
        if (s[i] == c)
            return i;
    }

    return inexistent_index; //0xFFFFFFFF
}

作为指向字符的指针

unsigned int FindCharFirst(char c, char* s)
{
    char* t;
    if (!s) {
        cout << "FindCharFirst: Illegal string.\n";
        exit(2);
    }
    for (t = s; *t != '\0'; t++) {
        if (*t == c)
            return t - s;
    }

    return inexistent_index;
}

作为抽象的字符串整体

  • 抽象字符串的定义: 用 char * 表达一个字符串
typedef char * STRING;
typedef const char * CSTRING;
  • FindCharFirst 函数的声明格式,下述三种声明格式完全相同
// 字符数组
unsigned int FindCharFirst(char c, char s[]);
// 字符指针
unsigned int FindCharFirst(char c, char * s);
// 抽象字符串
unsigned int FindCharFirst(char c, STRING s);

2.字符数组与字符指针的差异

  • 字符数组量的定义、初始化与存储 - char s[9] = {'C', 'P', 'P', '-', 'P', 'r', 'o', 'g', '\0'};
  • 字符指针量的定义、初始化与存储 - char * s = "CPP-Prog"; - s 作为一个指针,会分配一段存储空间, 4 个字节,里面保存字符串的基地址,而那个字符串的基地址里面会分配9个字节来存 “CPP-Prog\0”, \0 在这里是编译器自动添加的 - 后者存在单独的指针变量 s,前者没有
  • 按指针格式定义字符串,可以直接赋值 - char * s = "CPP-Prog"; 正确
  • 按字符数组格式定义字符串,不能直接赋值 - char s[9]; s = "CPP-Prog"; 错误 - 不能对数组进行整体赋值操作 - 原因:数组空间已经分配,字符串文字空间已经分配,且它们位于不同的位置,不能直接整体赋值 - char s[9] = "CPP-Prog"; 正确, 这里编译字符串一个一个写进数组里面去
//编写函数,将某个字符c 转换成字符串
typedef char * STRING;
STRING TransformCharIntoString(char c)
{
    STRING _s = (STRING)malloc(2);
    _s[0] = c;
    _s[1] = '\0';
    return _s;
}

因为字符和字符串是有区别的,因为字符串是以 \0结尾,所以这里使用 malloc(2) 为这个单字符的字符串分配了两个字节的存储空间,然后把这个空间转换成STRING。但是如果像下面这么写就是错误的

char * TransformCharIntoString(char c)
{
    char _s[2];
    _s[0] = c;
    _s[1] = '\0';
    return _s;
}

这个错误非常典型,是个错误的函数定义: 对于所有返回值为指针类型的函数,都不能返回在函数内部定义的局部数据对象–所有局部对象在结束后不再有效,所以其地址在函数返回后没有意义

3.标准字符串库

字符串库头文件

标准字符串库定义在头文件中,下面两个头文件的实现方式并不一样

  • C: #include <string.h>
  • CPP: #include <cstring>

常用字符串函数

如果写 c 程序,就按下面的标准库来,写 cpp 的时候就不建议使用下列标准库,建议使用 string 类来代替:

  • char * strcat(char * dest, const char * src);: 合并两个字符串
  • char * strcpy(char * dest, const char * src);: 拷贝一个字符串
  • int strcmp(const char * s1, const char * s2);: 比较两个字符串
  • int strlen(const char * s);: 求字符串的串长
  • char * strtok(char * token, const char * delimiters);: 解析串中所有的标记

string 类

如果写纯 cpp 程序而不写操作系统相关程序,string类就可以了:

  • 定义在头文件 string
  • 声明与构造 string 对象: - 声明: string s = "abcdefg"; - 构造: string s("abcdefg");
  • 读取与写入 string 对象 - cout << s << endl; - cin >> s;: 读取以空格、制表符、与回车符分隔的单词 - getline(cin, s, '\n');:读取包含空格和制表符在内的整行
  • 读取 string 对象的长度
    string s = "abcdefg";
    int a = s.length();
    
  • 改变 string 对象的容量大小 - s.resize(32);: 将 s 设为32字符长,多余的字符就舍弃,字符不足的话就空着 - s.resize(32, '=');: 多余舍弃,不足就在字符串后面补 ‘=’,然后占满整个空间
  • string 对象的追加操作
string s1 = "abcd", s2 = "efg";
s1.append(s2);
s1 += s2;
//两种方式都可以将字符串 s2追加到 s1 的底部
  • string 对象的比较操作

使用比较函数,比较函数相当于关系操作符, s1是左操作符, s2是右操作符

//比较 s1 和 s2 两个字符串的大小关系
string s1 = "abcdefg", s2 = "abcdexy";
// 从0号位开始比较
int a = s1.compare(s2, 0);
  • string 对象查找操作
string s1 = "abcdefg", s2 = "bcd";
//从字符串头开始查找,结果为 s2 在 s1首次出现的位置
int a = s1.find (s2, 0);

III.动态存储管理

1.内存分配与释放

静态内存分配方式

  • 适用对象:全局变量与静态局部变量
  • 分配与释放时机: 在程序运行前分配,程序结束时释放,实际上生存期比 main 函数还要长,main函数之前分配完成,main函数做完啦才能够销毁

    自动内存分配方式

  • 适用对象: 普通局部变量
  • 分配与释放时机: 在程序进入该函数或该块时自动进行,退出时自动释放,

    动态内存分配方式

  • 适用对象: 匿名数据对象(指针指向的目标数据对象)
  • 分配与释放时机: 在执行特定代码段时,按照该代码段的要求动态分配和释放。分配就用 malloc 或者 new, 销毁就 free 或者 delete
  • 目的 - 静态与自动内存分配方式必须事先了解数据对象的格式与存储空间大小 - 而部分场合是无法确定数据对象的大小的 - 示例: 声明一个包含 n 个元素的整数数组, n 的值由用户在执行程序执行时输入, 编译时程序未执行,不知道 n 的值!,所以动态分配就是一个合适的解决方法
  • 动态内存分配的位置 - 计算机维护的一个专门存储区: 堆 heap - 所有的动态分配内存都位于堆中
  • 动态内存分配的关键技术 - 使用指针指向动态分配的内存区 - 使用引领操作符操作目标的数据对象

2.标准库的动态存储管理函数

动态存储管理函数的原型

  • 头文件: cstdlibcmalloc, 两个包含其中一个即可
  • 内存分配函数原型: void * malloc(unsigned int size);
  • 内存释放函数原型: void free(void * memblock);

void * 类型

非常非常重要!!!, (哑型指针)

  • 特殊的指针类型, 指向的目标数据对象类型未知, 这里 void不是说指针的目标数据对象没有类型,类型是什么呢,是不知道的
  • 不能在 void * 指针上使用引领操作符去访问目标数据对象
  • 但是可以将它转化成任意的指针类型,不过转换后类型是否有意义,得看程序逻辑
  • 可以在转换后的类型上使用引领操作符
  • 主要目的:将这个指针视为一种通用的指针类型,首先构造只针对象与目标数据对象的一般性关联,然后由程序员在未来明确该关联的性质。这个 void * 指针可能什么类型都不能表示,但一定能表示这个指针指向的某数据对象的内存位置的地址

内存分配函数 malloc

  • 首先定义特定类型的指针变量: char * p;
  • 调用 malloc 函数分配内存: p = (char *)malloc(11);
  • 参数表示所需要分配的存储空间大小, 以字节为单位
  • 示例: 若要分配能够保存 10 个字符的字符串, 分配 11 个字节(字符串结束标志 ‘\0’也要分配空间)
  • 将返回值转换为 char * 类型赋值给原指针, 使 p 指向新分配空间的匿名目标数据对象
//编写函数,复制字符串
char * DuplicateString(char *s)
{
    char * t;
    unsigned int n, i;
    if(!s)
    {
        cout << "DuplicateString: Parameter Illegal.";
        exit(1);
    }
    n = strlen(s);
    t = (char *)malloc(n + 1);
    for(i = 0; i <  n; i++)
    {
        t[i] = s[i];
    }

    t[n] = '\0';

    return t;
}

内存释放函数 free

free函数是用来销毁 malloc 出来的空间,不是 malloc 出来的空间, 则不需要 free, 用法如下:

  • 传递一个指向动态内存分配的目标数据对象的指针
  • 示例一:
char * p;
p = (char *)malloc(11);
free(p);

销毁的不是 p 这个指针量,而是 p 指向的那个目标字符串所在的存储空间

  • 示例二:
int * p = (int *)malloc(10 * sizeof(int));
free(p);

这里用 malloc 分配能够容纳 10 个整数的连续存储空间,使 p 指向该空间的基地址, 最后调用 free 函数释放 p 指向的整个空间。虽然 p 指向的是数组的 0 号元,但是 free 销毁的是数组这个整体,而不是数组的 0 号元

有分配就要有释放: 有 malloc 就一定要有 free, free 函数释放的是 p 指向的目标数据对象的空间,而不是 p 本身的存储空间,调用 free 函数后, p 指向的空间不再有效,但 p 仍然指向它; 为保证在释放目标数据对象空间后,不会再次使用 p 访问,建议按照下述格式书写代码: free(p); p = NULL;

C++的内存分配操作符:new 与 delete

new 和 delete 比 malloc 和 free 要更方便,因为不仅可以分配动态内存空间,还可以构造目标数据对象

动态创建单个目标数据对象
  • 分配目标对象: int * p; p = new int; *p = 10;
  • 分配目标对象: int * p; p = new(int); *p = 10;
  • 分配目标对象并初始化: int *p; p = new int(10); 将 *p 初始化为 10
  • 分配目标对象并初始化: int *p; p = new (int)(10); 将 *p 初始化为 10
动态创建多个目标数据对象
  • 分配数组目标对象: int * p; p = new int[8]; 分配8个元素的整数数组, 把数组的基地址赋值给p
释放单个目标数据对象
  • 释放目标对象:int * p; p = new int; *p = 10; delete p;
释放多个目标数据对象
  • 释放目标对象:int * p; p = new int[8]; delete[] p;

注意: malloc 和 free 是配对的, new 和 delete 是配对的,new[] 和 delete[] 是配对的; 用 C 的方式创建的内存,一定要用 C 的方式去销毁, CPP 方式同理

所有权与空悬指针

目标数据对象的所有权

  • 指向该目标数据对象的指针对象拥有所有权
  • 在程序中要时刻明确动态分配内存的目标数据对象的所有权属于哪个数据对象

指针使用的一般原则

  • 主动释放原则:如果某函数动态分配了内存,在函数退出时该目标数据对象不再需要,应该主动释放它,此时 malloc 与 free 在函数中成对出现
  • 所有权转移原则: 如果某函数动态分配了内存,在函数退出后,该目标数据对象仍然需要,此时应将其所有权转交给本函数之外的同型指针对象,函数内部代码只有malloc, 没有free,往往都是通过函数返回值来转移所有权的

空悬指针问题

  • 所有权的重叠: 指针赋值操作导致两个指针数据对象指向同样的目标数据对象,即两个指针都声称“自己拥有目标对象的所有权”
  • 示例: int * p, * q; q = (int *) malloc(sizeof(int)); p = q;
  • 产生原因:如果在程序中,通过某个指针释放了目标数据对象,另一个指针并不了解这种状况,它仍指向不再有效的目标数据对象,导致空悬指针
  • 示例: free(p); p = NULL; 这里 q 就空悬了,一旦再用 q 去访问目标数据对象,那么, 程序就会崩溃
  • 解决方案: 确保程序中只有唯一一个指针拥有目标数据对象,即只有它负责目标数据对象的存储管理,其它指针只可访问,不可管理; 若目标数据对象仍有存在价值,但该指针不再有效,此时应该将所有权移交; 在一个函数里面,确保最多只有一个指针拥有目标数据对象,其它指针即使存在,也仅能访问,不可管理; 如果可能,在分配目标数据对象动态内存的函数中,如 main 函数分配的内存在 main 函数中释放; 退一步,如果上述条件不满足,在分配目标数据对象动态内存的函数的主调函数中释放掉内存,即将所有权移交给上级函数,级级上报,层层审批

3.内存泄漏与垃圾回收

内存泄漏问题

  • 产生原因:若某个函数通过局部指针变量动态分配了一个目标数据对象内存,在函数调用结束后没有释放该内存,并且所有权没有被移交,那么在内存中,就生成了一个垃圾
  • 示例: void f(){ int * p = new int; *p = 10;} 函数 f 结束后, p 不再存在, *p 所在的存储空间仍在, 10 仍然存在,但没有任何指针对象拥有它,所以这个内存是不可访问的
  • 问题的实质:动态分配的内存必须动态释放,函数本身并不负责管理它
  • 垃圾回收机制: 系统负责管理,程序员不需主动释放动态分配的内存, Java 有这个功能, C/C++ 语言没有; 但是, 垃圾回收机制,在需要的时候,效率很差,不需要的时候,效率很好

IV.引用

1.引用类型

引用的定义

  • 格式: 数据类型 & 变量名称 = 被引用变量名称;
  • 示例: int a; int & ref = a;

引用的性质

  • 引用类型的变量不占用单独的存储空间
  • 为另一数据对象起个别名,与该对象同享存储空间

特殊说明

  • 引用类型的变量必须在定义的时候初始化,除非引用类型的量作为函数的参数,可以不用初始化
  • 此关联关系在引用类型变量的整个存续期都保持不变
  • 对引用类型变量的操作就是对被引用变量的操作

引用示例

#include <iostream>
using namespace std;
int main(){
    int a;
    int & ref = a;
    a = 5;
    cout << "a: " << a << endl;
    cout << "ref: " << a << endl;
    ref = 8;
    cout << "a: " << a << endl;
    cout << "ref: " << a << endl;
    return 0;

}

2.引用函数作为函数参数

引用的最大意义:作为函数参数

  • 参数传递机制:引用传递, 直接修改实际参数值,不是值传递,是传递的原始数据的一个引用
  • 使用格式:返回值类型 函数名称(类型 & 参数名称);
  • 函数原型示例: void Swap(int & x, int & y);
  • 函数实现示例:
void Swap(int & x, int & y){
    int t;
    t = x;
    x = y;
    y = t;
    return;

}
  • 函数调用示例:
int main(){
    int a = 10, b = 20;
    Swap(a, b);
    // 在Swap函数内部,x 就相当与 a 的别名, y 就相当于 b 的别名
    // 所以在Swap里 x 和 y的改变,就相当于 main 函数里 a, b 的改变
    return 0;
}

引用作为函数的返回值

  • 常量引用: 仅能引用常量, 不能通过引用改变目标对象值; 引用本身也不能改变引用对象
  • 引用作为函数返回值时不生成副本
  • 函数原型示例:int & Inc(int & dest, const int & alpha);
  • 函数实现示例:
int & Inc(int & dest, const int & alpha){
    dest += alpha;
    return dest;
}
  • 函数调用示例:引用类型返回值可以自增
int main(){
    int a = 10, b  = 20, c;
    Inc(a,b);
    c = Inc(a, b)++;
    return 0;
}

Share this on