1. 指针是一种数据类型

1.1 指针变量

指针是一种数据类型,占用内存空间,用来保存内存地址。

  1. void test01(){
  2. int* p1 = 0x1234;
  3. int*** p2 = 0x1111;
  4. printf("p1 size:%d\n",sizeof(p1));
  5. printf("p2 size:%d\n",sizeof(p2));
  6. //指针是变量,指针本身也占内存空间,指针也可以被赋值
  7. int a = 10;
  8. p1 = &a;
  9. printf("p1 address:%p\n", &p1);
  10. printf("p1 address:%p\n", p1);
  11. printf("a address:%p\n", &a);
  12. }

1.2 野指针和空指针

1.2.1 空指针

标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针为NULL,可以给它赋值一个零值。为了测试一个指针百年来那个是否为NULL,你可以将它与零值进行比较。
对指针解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西,因为对一个NULL指针因引用是一个非法的操作,在解引用之前,必须确保它不是一个NULL指针。

如果对一个NULL指针间接访问会发生什么呢?结果因编译器而异。

不允许向NULL和非法地址拷贝内存:

  1. void test(){
  2. char *p = NULL;
  3. //给p指向的内存区域拷贝内容
  4. strcpy(p, "1111"); //err
  5. char *q = 0x1122;
  6. //给q指向的内存区域拷贝内容
  7. strcpy(q, "2222"); //err
  8. }

1.2.2 野指针

在使用指针时,要避免野指针的出现:
野指针指向一个已删除的对象或未申请访问受限内存区域的指针。与空指针不同,野指针无法通过简单地判断是否为 NULL 避免,而只能通过养成良好的编程习惯来尽力减少。对野指针进行操作很容易造成程序错误。

什么情况下回导致野指针?

  1. 指针变量未初始化

任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

  1. 指针释放后未置空

有时指针在free或delete后未赋值 NULL,便会使人以为是合法的。别看free和delete的名字(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。此时指针指向的就是“垃圾”内存。释放后的指针应立即将指针置为NULL,防止产生“野指针”。

  1. 指针操作超越变量作用域

不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放。

规避野指针的出现

  1. 初始化时置 NULL

指针变量一定要初始化为NULL,因为任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的。

  1. 释放时置 NULL

当指针p指向的内存空间释放时,没有设置指针p的值为NULL。delete和free只是把内存空间释放了,但是并没有将指针p的值赋为NULL。通常判断一个指针是否合法,都是使用if语句测试该指针是否为NULL。

1.3 间接访问操作符

通过一个指针访问它所指向的地址的过程叫做间接访问,或者叫解引用指针,这个用于执行间接访问的操作符是*
注意:对一个int 类型指针解引用会产生一个整型值,类似地,对一个float指针解引用会产生了一个float类型的值。

注:

  1. 在指针声明时,* 号表示所声明的变量为指针
  2. 在指针使用时,* 号表示操作指针所指向的内存空间
    • 相当通过地址(指针变量的值)找到指针指向的内存,再操作内存
    • 放在等号的左边赋值(给内存赋值,写内存)
    • 放在等号的右边取值(从内存中取值,读内存)

示例代码:

  1. // 解引用
  2. void test01(){
  3. //定义指针
  4. int* p = NULL;
  5. //指针指向谁,就把谁的地址赋给指针
  6. int a = 10;
  7. p = &a;
  8. *p = 20;//*在左边当左值,必须确保内存可写
  9. //*号放右面,从内存中读值
  10. int b = *p;
  11. //必须确保内存可写
  12. char* str = "hello world!";
  13. *str = 'm';
  14. printf("a:%d\n", a);
  15. printf("*p:%d\n", *p);
  16. printf("b:%d\n", b);
  17. }

1.4 指针的步长

指针是一种数据类型,是指它指向的内存空间的数据类型。指针所指向的内存空间决定了指针的步长。指针的步长指的是,当指针+1时候,移动多少字节单位。

思考如下问题:

  1. int a = 0xaabbccdd;
  2. unsigned int *p1 = &a;
  3. unsigned char *p2 = &a;
  4. //为什么*p1打印出来正确结果?
  5. printf("%x\n", *p1);
  6. //为什么*p2没有打印出来正确结果?
  7. printf("%x\n", *p2);
  8. //为什么p1指针+1加了4字节?
  9. printf("p1 =%d\n", p1);
  10. printf("p1+1=%d\n", p1 + 1);
  11. //为什么p2指针+1加了1字节?
  12. printf("p2 =%d\n", p2);
  13. printf("p2+1=%d\n", p2 + 1);

2. 指针的意义_间接赋值

2.1 间接赋值的三大条件

通过指针间接赋值成立的三大条件:

  1. 2个变量(一个普通变量一个指针变量、或者一个实参一个形参)
  2. 建立关系
  3. 通过 * 操作指针指向的内存
    1. void test(){
    2. int a = 100; //两个变量
    3. int *p = NULL;
    4. //建立关系
    5. //指针指向谁,就把谁的地址赋值给指针
    6. p = &a;
    7. //通过*操作内存
    8. *p = 22;
    9. }

2.2 如何定义合适的指针变量

  1. void test(){
  2. int b;
  3. int *q = &b; //0级指针
  4. int **t = &q;
  5. int ***m = &t;
  6. }

2.3 间接赋值:从0级指针到1级指针

  1. int func1(){ return 10; }
  2. void func2(int a){
  3. a = 100;
  4. }
  5. //指针的意义_间接赋值
  6. void test02(){
  7. int a = 0;
  8. a = func1();
  9. printf("a = %d\n", a);
  10. //为什么没有修改?
  11. func2(a);
  12. printf("a = %d\n", a);
  13. }
  14. //指针的间接赋值
  15. void func3(int* a){
  16. *a = 100;
  17. }
  18. void test03(){
  19. int a = 0;
  20. a = func1();
  21. printf("a = %d\n", a);
  22. //修改
  23. func3(&a);
  24. printf("a = %d\n", a);
  25. }

2.4 间接赋值:从1级指针到2级指针

  1. void AllocateSpace(char** p){
  2. *p = (char*)malloc(100);
  3. strcpy(*p, "hello world!");
  4. }
  5. void FreeSpace(char** p){
  6. if (p == NULL){
  7. return;
  8. }
  9. if (*p != NULL){
  10. free(*p);
  11. *p = NULL;
  12. }
  13. }
  14. void test(){
  15. char* p = NULL;
  16. AllocateSpace(&p);
  17. printf("%s\n",p);
  18. FreeSpace(&p);
  19. if (p == NULL){
  20. printf("p内存释放!\n");
  21. }
  22. }

2.5 间接赋值的推论

  1. 用1级指针形参,去间接修改了0级指针(实参)的值。
  2. 用2级指针形参,去间接修改了1级指针(实参)的值。
  3. 用3级指针形参,去间接修改了2级指针(实参)的值。
  4. 用n级指针形参,去间接修改了n-1级指针(实参)的值。

3. 指针做函数参数

指针做函数参数,具备输入和输出特性:

  1. 输入:主调函数分配内存
  2. 输出:被调用函数分配内存

3.1 输入特性

  1. void fun(char *p /* in */)
  2. {
  3. //给p指向的内存区域拷贝内容
  4. strcpy(p, "abcddsgsd");
  5. }
  6. void test(void)
  7. {
  8. //输入,主调函数分配内存
  9. char buf[100] = { 0 };
  10. fun(buf);
  11. printf("buf = %s\n", buf);
  12. }

3.2 输出特性

  1. void fun(char **p /* out */, int *len)
  2. {
  3. char *tmp = (char *)malloc(100);
  4. if (tmp == NULL)
  5. {
  6. return;
  7. }
  8. strcpy(tmp, "adlsgjldsk");
  9. //间接赋值
  10. *p = tmp;
  11. *len = strlen(tmp);
  12. }
  13. void test(void)
  14. {
  15. //输出,被调用函数分配内存,地址传递
  16. char *p = NULL;
  17. int len = 0;
  18. fun(&p, &len);
  19. if (p != NULL)
  20. {
  21. printf("p = %s, len = %d\n", p, len);
  22. }
  23. }

4. 字符串指针强化

4.1 字符串指针做函数参数

4.1.1 字符串基本操作

  1. //字符串基本操作
  2. //字符串是以0或者'\0'结尾的字符数组,(数字0和字符'\0'等价)
  3. void test01(){
  4. //字符数组只能初始化5个字符,当输出的时候,从开始位置直到找到0结束
  5. char str1[] = { 'h', 'e', 'l', 'l', 'o' };
  6. printf("%s\n",str1);
  7. //字符数组部分初始化,剩余填0
  8. char str2[100] = { 'h', 'e', 'l', 'l', 'o' };
  9. printf("%s\n", str2);
  10. //如果以字符串初始化,那么编译器默认会在字符串尾部添加'\0'
  11. char str3[] = "hello";
  12. printf("%s\n",str3);
  13. printf("sizeof str:%d\n",sizeof(str3));
  14. printf("strlen str:%d\n",strlen(str3));
  15. //sizeof计算数组大小,数组包含'\0'字符
  16. //strlen计算字符串的长度,到'\0'结束
  17. //那么如果我这么写,结果是多少呢?
  18. char str4[100] = "hello";
  19. printf("sizeof str:%d\n", sizeof(str4));
  20. printf("strlen str:%d\n", strlen(str4));
  21. //请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
  22. char str5[] = "hello\0world";
  23. printf("%s\n",str5);
  24. printf("sizeof str5:%d\n",sizeof(str5));
  25. printf("strlen str5:%d\n",strlen(str5));
  26. //再请问下面输入结果是多少?sizeof结果是多少?strlen结果是多少?
  27. char str6[] = "hello\012world";
  28. printf("%s\n", str6);
  29. printf("sizeof str6:%d\n", sizeof(str6));
  30. printf("strlen str6:%d\n", strlen(str6));
  31. }

八进制和十六进制转义字符:
在C中有两种特殊的字符,八进制转义字符和十六进制转义字符,八进制字符的一般形式是’\ddd’,d是0-7的数字。十六进制字符的一般形式是’\xhh’,h是0-9或A-F内的一个。八进制字符和十六进制字符表示的是字符的ASCII码对应的数值。
比如 :
n ‘\063’表示的是字符’3’,因为’3’的ASCII码是30(十六进制),48(十进制),63(八进制)。
‘\x41’表示的是字符’A’,因为’A’的ASCII码是41(十六进制),65(十进制),101(八进制)。

4.1.2 字符串拷贝功能实现

  1. //拷贝方法1
  2. void copy_string01(char* dest, char* source ){
  3. for (int i = 0; source[i] != '\0';i++){
  4. dest[i] = source[i];
  5. }
  6. }
  7. //拷贝方法2
  8. void copy_string02(char* dest, char* source){
  9. while (*source != '\0' /* *source != 0 */){
  10. *dest = *source;
  11. source++;
  12. dest++;
  13. }
  14. }
  15. //拷贝方法3
  16. void copy_string03(char* dest, char* source){
  17. //判断*dest是否为0,0则退出循环
  18. while (*dest++ = *source++){}
  19. }

4.1.3 字符串反转模型

image.png

  1. void reverse_string(char* str){
  2. if (str == NULL){
  3. return;
  4. }
  5. int begin = 0;
  6. int end = strlen(str) - 1;
  7. while (begin < end){
  8. //交换两个字符元素
  9. char temp = str[begin];
  10. str[begin] = str[end];
  11. str[end] = temp;
  12. begin++;
  13. end--;
  14. }
  15. }
  16. void test(){
  17. char str[] = "abcdefghijklmn";
  18. printf("str:%s\n", str);
  19. reverse_string(str);
  20. printf("str:%s\n", str);
  21. }

4.2字符串的格式化

4.2.1 sprintf()

  1. #include <stdio.h>
  2. int sprintf(char *str, const char *format, ...);

功能:
根据参数format字符串来转换并格式化数据,然后将结果输出到str指定的空间中,直到 出现字符串结束符 ‘\0’ 为止。
参数:

  • str:字符串首地址
  • format:字符串格式,用法和printf()一样

返回值:

  • 成功:实际格式化的字符个数
  • 失败: -1
  1. void test(){
  2. //1. 格式化字符串
  3. char buf[1024] = { 0 };
  4. sprintf(buf, "你好,%s,欢迎加入我们!", "John");
  5. printf("buf:%s\n",buf);
  6. memset(buf, 0, 1024);
  7. sprintf(buf, "我今年%d岁了!", 20);
  8. printf("buf:%s\n", buf);
  9. //2. 拼接字符串
  10. memset(buf, 0, 1024);
  11. char str1[] = "hello";
  12. char str2[] = "world";
  13. int len = sprintf(buf,"%s %s",str1,str2);
  14. printf("buf:%s len:%d\n", buf,len);
  15. //3. 数字转字符串
  16. memset(buf, 0, 1024);
  17. int num = 100;
  18. sprintf(buf, "%d", num);
  19. printf("buf:%s\n", buf);
  20. //设置宽度 右对齐
  21. memset(buf, 0, 1024);
  22. sprintf(buf, "%8d", num);
  23. printf("buf:%s\n", buf);
  24. //设置宽度 左对齐
  25. memset(buf, 0, 1024);
  26. sprintf(buf, "%-8d", num);
  27. printf("buf:%s\n", buf);
  28. //转成16进制字符串 小写
  29. memset(buf, 0, 1024);
  30. sprintf(buf, "0x%x", num);
  31. printf("buf:%s\n", buf);
  32. //转成8进制字符串
  33. memset(buf, 0, 1024);
  34. sprintf(buf, "0%o", num);
  35. printf("buf:%s\n", buf);
  36. }

4.2.2 sscanf()

#include <stdio.h>
int sscanf(const char *str, const char *format, ...);

功能:
从str指定的字符串读取数据,并根据参数format字符串来转换并格式化数据。
参数:

  • str:指定的字符串首地址
  • format:字符串格式,用法和scanf()一样

返回值:

  • 成功:成功则返回参数数目,失败则返回-1
  • 失败: -1
格式 作用
%s或%d 跳过数据
%[width]s 读指定宽度的数据
%[a-z] 匹配a到z中任意字符(尽可能多的匹配)
%[aBc] 匹配a、B、c中一员,贪婪性
%[^a] 匹配非a的任意字符,贪婪性
%[^a-z] 表示读取除a-z以外的所有字符
//1. 跳过数据
void test01(){
    char buf[1024] = { 0 };
    //跳过前面的数字
    //匹配第一个字符是否是数字,如果是,则跳过
    //如果不是则停止匹配
    sscanf("123456aaaa", "%*d%s", buf); 
    printf("buf:%s\n",buf);
}

//2. 读取指定宽度数据
void test02(){
    char buf[1024] = { 0 };
    //跳过前面的数字
    sscanf("123456aaaa", "%7s", buf);
    printf("buf:%s\n", buf);
}

//3. 匹配a-z中任意字符
void test03(){
    char buf[1024] = { 0 };
    //跳过前面的数字
    //先匹配第一个字符,判断字符是否是a-z中的字符,如果是匹配
    //如果不是停止匹配
    sscanf("abcdefg123456", "%[a-z]", buf);
    printf("buf:%s\n", buf);
}

//4. 匹配aBc中的任何一个
void test04(){
    char buf[1024] = { 0 };
    //跳过前面的数字
    //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
    sscanf("abcdefg123456", "%[aBc]", buf);
    printf("buf:%s\n", buf);
}

//5. 匹配非a的任意字符
void test05(){
    char buf[1024] = { 0 };
    //跳过前面的数字
    //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
    sscanf("bcdefag123456", "%[^a]", buf);
    printf("buf:%s\n", buf);
}

//6. 匹配非a-z中的任意字符
void test06(){
    char buf[1024] = { 0 };
    //跳过前面的数字
    //先匹配第一个字符是否是aBc中的一个,如果是,则匹配,如果不是则停止匹配
    sscanf("123456ABCDbcdefag", "%[^a-z]", buf);
    printf("buf:%s\n", buf);
}

课堂小练习:

  1. 已给定字符串为: helloworld@itcast.cn,请编码实现helloworld输出和itcast.cn输出。
  2. 已给定字符串为:123abcd$myname@000qwe.请编码实现匹配出myname字符串,并输出.

5. 一级指针易错点

5.1 越界

void test(){
    char buf[3] = "abc";
    printf("buf:%s\n",buf);
}

5.2 指针叠加会不断改变指针指向

void test(){
    char *p = (char *)malloc(50);
    char buf[] = "abcdef";
    int n = strlen(buf);
    int i = 0;

    for (i = 0; i < n; i++)
    {
        *p = buf[i];
        p++; //修改原指针指向
    }

    free(p);
}

5.3 返回局部变量地址

char *get_str()
{
    char str[] = "abcdedsgads"; //栈区,
    printf("[get_str]str = %s\n", str);
    return str;
}

5.4 同一块内存释放多次(不可以释放野指针)

void test(){    
    char *p = NULL;

    p = (char *)malloc(50);
    strcpy(p, "abcdef");

    if (p != NULL)
    {
        //free()函数的功能只是告诉系统 p 指向的内存可以回收了
        // 就是说,p 指向的内存使用权交还给系统
        //但是,p的值还是原来的值(野指针),p还是指向原来的内存
        free(p); 
    }

    if (p != NULL)
    {
        free(p);
    }
}

6. const使用

//const修饰变量
void test01(){
    //1. const基本概念
    const int i = 0;
    //i = 100; //错误,只读变量初始化之后不能修改

    //2. 定义const变量最好初始化
    const int j;
    //j = 100; //错误,不能再次赋值

    //3. c语言的const是一个只读变量,并不是一个常量,可通过指针间接修改
    const int k = 10;
    //k = 100; //错误,不可直接修改,我们可通过指针间接修改
    printf("k:%d\n", k);
    int* p = &k;
    *p = 100;
    printf("k:%d\n", k);
}

//const 修饰指针
void test02(){

    int a = 10;
    int b = 20;
    //const放在*号左侧 修饰p_a指针指向的内存空间不能修改,但可修改指针的指向
    const int* p_a = &a;
    //*p_a = 100; //不可修改指针指向的内存空间
    p_a = &b; //可修改指针的指向

    //const放在*号的右侧, 修饰指针的指向不能修改,但是可修改指针指向的内存空间
    int* const p_b = &a;
    //p_b = &b; //不可修改指针的指向
    *p_b = 100; //可修改指针指向的内存空间

    //指针的指向和指针指向的内存空间都不能修改
    const int* const p_c = &a;
}
//const指针用法
struct Person{
    char name[64];
    int id;
    int age;
    int score;
};

//每次都对对象进行拷贝,效率低,应该用指针
void printPersonByValue(struct Person person){
    printf("Name:%s\n", person.name);
    printf("Name:%d\n", person.id);
    printf("Name:%d\n", person.age);
    printf("Name:%d\n", person.score);
}

//但是用指针会有副作用,可能会不小心修改原数据
void printPersonByPointer(const struct Person *person){
    printf("Name:%s\n", person->name);
    printf("Name:%d\n", person->id);
    printf("Name:%d\n", person->age);
    printf("Name:%d\n", person->score);
}
void test03(){
    struct Person p = { "Obama", 1101, 23, 87 };
    //printPersonByValue(p);
    printPersonByPointer(&p);
}