07- c语言指针 (C语言)

一 指针的引入

1、一般把内存中的一个字节称为一个内存单元。
2、为了正确地访问这些内存单元,必须为每个内存单元编上号。根据一个内存单元的编号即可准确地找到该内存单元。内存单元的编号也叫做地址,通常也把这个地址称为指针
3、如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)。
4、变量的地址就是变量的指针,存放变量地址的变量是指针变量
5、内存单元的指针内存单元的内容是两个不同的概念。可以用一个通俗的例子来说明它们之间的关系。我们到银行去存取款时,银行工作人员将根据我们的帐号去找我们的存款单,找到之后在存单上写入存款、取款的金额。在这里,帐号就是存单的指针,存款数是存单的内容。对于一个内存单元来说,单元的地址即为指针,其中存放的数据才是该单元的内容。

二 指针变量的定义和使用

2.1 指针变量定义语法

数据类型  *指针变量名,注意:
1、数据类型为C语言支持的 所有数据类型
2、指针变量名遵循C语言变量的 命名规则

举例:

int *p; //定义了一个指针变量p,简称指针p,p是变量, int *是类型
char* p2;

我们也认为 指针是一种数据类型

2.2 指针变量的赋值

1)指针变量的值代表这个指针指向了以这个值为首地址的那块 内存空间

2)指针变量赋值 为其他变量的地址:&:取地址运算符

//指针变量 = &变量名;
&:取地址运算符
int a = 10;
int *p = &a;

3)指针变量 操作指向的内存空间
可以通过 指针变量访问 和修改所指向的内存空间中的内容

*:指针运算符(或称“间接访问” 运算)。
int a = 10;
int *p;
p = &a;
printf("*p: %d\n", *p);
*p = 100;
printf("*p: %d\n", *p);

4)指针变量同普通变量一样,定义后如果不进行初始化指针变量的值是不确定的

5)野指针
因为指针变量的值是不确定的,所以这个指针我们称之为“野指针”。
野指针的危害:因为指针指向的空间是不确定的,所以指针可能会操作到非法的内存空间,导致程序运行奔溃。 

int a = 100;
int *p;
*p = 1000;
/*因为p没有初始化/赋值,所以p的值是不确定的,如果此时p的值恰好等于a的地址(p == &a), 那么*p=1000将a的值修改为如果p的值恰好是内存上一块只读的内存空间,*p = 1000将导致程序异常退出,你可能会看到程序运行报错(段错误/核心内容*/

6)空指针
为了标志指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针。

int *p = NULL;

NULL是一个值为0的宏常量:

#define NULL ((void *)0)

注意:空指针的作用是防止指针变量变成野指针。如果用*访问空指针所指向的内存空间也会程序报错。

7)笔试题:嵌入式系统经常具有要求程序员去 访问某特定的内存位置 的特点。在某工程中,要求设置一绝对地址为 0x67a9 的整型变量的值为 0xaa66

//方法1:
int *ptr;
ptr = (int *)0x67a9; //在内存地址编号的前面加上(int *)将地址编号这个无符号整型数据强制转换为
//(int*)指针类型,这样赋值符号左值和右值的数据类型一致
*ptr = 0xaa66;
//方法2:
*(int *)(0x67a9) = 0xaa55;

注意:在实际工作中我们一般 很少会将一个确定的内存地址赋值给一个指针变量,因为程序员一般不知道哪个内存地址是可用的!!!

2.3 不同类型指针变量之间的区别

1、int *p1 和 char *p2 的相同点是什么?

int x = 100;
int *p1 = &x;
char y = 'A';
char *p2 = &y;

相同点:

  • 都是指针变量
  • 都是用来保存一个内存地址编号
  • 占用的内存空间大小一样
int *p1;
char *p2;
printf("%d %d\n", sizeof(p1), sizeof(p2));

我们发现p1和p2占用的内存空间为4/8,在32位机器上结果为4, 64位机器上结果为8。
思考:

为什么指针变量占用的内存空间是 4 或者 8个字节呢?
因为指针变量保存的是一个内存地址的编号!
32位机器内存地址编号最大值为 2^32-1,可以用一个4字节的变量保存。
64位机器内存地址编号最大值为 2^64-1,可以用一个8字节的变量保存。

2、int *p1 和 char *p2 的不同点是什么?
首先我们应该知道:内存中存储的只是二进制而已。
之所以有 int float char 等数据类型是程序员希望将存储在内存中的二进制当作某种数据类型来处理而已。

int x = 65;
printf("%c\n", x);
  • int *p1的作用就是指针变量p1将他所指向的内存空间中的二进制当作 int类型 来处理。
  • char *p2的作用就是指针变量p2将他所指向的内存空间中的二进制当作 char类型 来处理。

3、p1++ 和 p2++的区别

#include <iostream>

int main() {
    int x = 10;
    int *p1 = &x;
    char y = 'A';
    char *p2 = &y;
    printf("p1: %p, p2: %p\n", p1, p2);
    p1++;
    p2++;
    printf("p1: %p, p2: %p\n", p1, p2);
    return 0;
}

p1自增后和自增前值 相差4
p2自增后和自增前值 相差1
指针变量+n,不是指针往后偏移n个字节,而是指针变量往后偏移n个数据类型,例如:p1+3,
表示指针p1往后偏移3个int类型的数据,指针变量p1的值+12 (3*sizeof(int)) 。

三 指针和数组

3.1 数组的指针

1、一个变量有一个地址,一个数组包含若干元素,每个数组元素都在内存中占用存储单元,它们都有相应的地址。所谓 数组的指针是指数组的起始地址
2、数组名表示数组的首地址,因此数组名也是一种指针
3、通过数组名访问数组中元素。

int ch[] = {1,2,3,4};
//假如我们想访问ch中的第3个元素:ch[3] == 4
//我们也可以通过指针法引用数组中的元素:比如 *(ch + 3)
//那么,如果想访问第n个元素呢?
*(ch + n) 注意:n<=sizeof(ch)/sizeof(ch[0])-1

4、练习:假如有数组int a[4], 编写代码实现如下功能:

  • 通过从键盘上输入数字对数组a的每一个元素进行赋值
  • 打印出数组a中每一个元素的地址
  • 通过指针法将数组a中的每一个元素的值打印出来
#include <iostream>

int main() {
    int a[4];
    int i;
    for (i = 0; i < 4; i++)
        scanf("%d", &a[i]);
    for (i = 0; i < 4; i++) {
        printf("%p %d\\n", &a[i], *(a+i));
    }
    }

5、通过指针变量间接访问数组

#include <iostream>

int main() {
    int a[4] = {1,2,3,4};
    int *p;
    p = a;
    *(p + 2) = 100;
    char ch[] = {'a', 'b', 'c'};
    char *p2;
    p2 = ch;
    *(p2+1) = 'A';
    }
#include <iostream>

int main() {
    int ch[] = {1, 2, 3, 4};
    printf("%d\n", ch[4]);
    printf("%p %p\n", &ch[3], &ch[4]);
    //数组名:数组的首地址
    printf("ch: %p\n", ch);
    //数组中的第0个元素的地址:数组的首地址
    printf("&ch[0]: %p\n", &ch[0]);
    printf("%d %d\n", ch[3], *(ch+3));
    int a[4];
    int i;
    for (i = 0; i < 4; i++)
    {
        scanf("%d", &a[i]); //a+i
        getchar();
    }
    //打印数组中每个元素的地址
    for (i = 0; i < 4; i++)
        printf("%p\n", &a[i]);
    //通过指针法将数组a中的每一个元素的值打印出来
    for (i = 0; i < 4; i++)
        printf("%d\n", *(a+i));
    int *p;
    p = a; //指针p指向了数组a

    //指针指向了一个数组,可以将指针当数组看待
    for (i = 0; i < 4; i++)
        printf("%d\n", p[i]); //通过下标发访问数组中的元素
        // printf("%d\n", *(p+i));
    }

6、数组指针越界

#include <iostream>

int main() {
    int b[4] = {10, 20, 30, 40};
    int a[4];
    a[4] = 100;
    printf("a[4]: %d\n", a[4]);
    printf("b[0]: %d\n", b[0]);
    printf("a: %u, b: %u\n", &a, &b); //打印数组a和b的首地址
    }

3.2 指针数组

1、指针数组顾名思义就是:存放指针的数组,本质是数组,数组中的每个元素都是指针 

#include <stdio.h>

int main()
{
    int a = 10, b = 20, c = 30;
    int *p[3];
    p[0] = &a;
    p[1] = &b;
    p[2] = &c;
    return 0;
}

2、注意:int *p[3]; 等价于 (int *) p[3]; 因为[] 比 *的优先级要高先与p匹配。
3、思考:如何通过一个数组存储10个人的姓名?
char *name[10] = {"zhangsan", "lisi", "wangwu", "zhaoliu, "tianqi"};
name数组中保存了10个字符串常量的首地址(注意:没有保存字符串常量而是常量的首地址)。

3.3 指针变量的地址

1、我们在定义一个 指针变量 的时候,编译器会分配一块空间来存储这个指针变量的值,分配的这块内存空间肯定有一个地址编码啦,那么这个地址编码肯定就是这个指针变量的地址啦。

#include <stdio.h>

int main()
{
    int a = 10;
    int *p;
    p = &a;
    //将指针变量p的值以及变量a的地址打印出来(结果应该是两者相等)
    printf("p: %p, &a: %p\n", p, &a);
    //打印指针变量p的地址(存储指针变量p的内存空间的首地址)
    printf("&p: %p\n", &p);
    return 0;
}

2、强调:指针变量p的值保存的是另外一个变量a的地址0x300800,指针变量的地址是存储p这个指针变量的值的那块内存空间的首地址:0x3007F8,这块空间中保存的值是0x300800 

3.4 一级指针作为函数的形参

1、函数的形参为数组
如果函数的形参是数组,该形参的定义方法如下:

void func(int a[], int n)
{}

我们也可以将形参定义为 指针类型
 

void func(int *a, int n)
{}

在实际工作中我们通常使用第二种方法!

2、调用函数时需要传递字符串可将形参设计为char *类型

#include <stdio.h>

void func(char *p) //调用函数时将字符串的地址赋值给指针变量p
{
    printf("%c\n", p[0]);   //返回值为 'h'
}
int main()
{
    func("hello");
    return 0;
}

3、当形参为数组时,如果获取数组的长度呢?     //sizeof( )

为什么sizeof(a)和sizeof(b)的值都是8呢?
原因:编译器在编译的时候将a和b当做了指针来处理了!!! 

4、注意:如果函数的 形参为指针,在函数体中一般先对指针的值进行判断,判断指针的值是否为NULL

3.5 二级指针

1、用一个指针变量保存一个一级指针变量的地址,这个指针我们称之为二级指针
2、二级指针的定义 :

  • 数据类型 **变量名;

3、二级指针的应用 

void func_1(){
    //二级指针的使用
    int a = 10;
    int *p = &a;
    int **p2 = &p; //二级指针p2保存了一级指针p的地址(p2指向了p )
    int ***p3 = &p2; //三级指针
    //*p2 == p == &a
    printf("%p %p %p\n", *p2, p, &a);  //000000000061FDE4 000000000061FDE4 
    //**p2 == *p == *(&a) == a
    printf("%d %d %d %d\n", **p2, *p, *(&a), a);   //10 10 10 10

    **p2 = 100;
    printf("%d %d %d %d\n", **p2, *p, *(&a), a);  //100 100 100 100
    //*p3 == p2 = &p
    //**p3 == *p2 == p == &a
    //***p3 == **p2 == *p == *(&a) == a
    printf("***p3: %d\n", ***p3);   //***p3: 100
}

3.6 内存分配

1、在实际工作中,如果我们需要存储多个数据很多同学首先想到的是使用数组,但是因为数组的长度在定义完后是固定的所以往往不够灵活
2、我们可以根据需要存储的数据类型先定义一个指针变量,例如:int *p; 然后根据实际需求使用
malloc函数
分配空间。

3、malloc函数

#include <stdlib.h>
void *malloc(size_t size);

功能:malloc 函数像系统申请size个字节的内存空间,并且返回一个指针,这个指针指向被分配的内存空间的首地址,并且申请的内存空间是在“堆”上的。堆上的空间是需要手动申请,手动释放的!!否则就会造成内存泄漏。

int *p;
//假如我们需要存储10个int类型的数据
p = (int *)malloc(10*sizeof(int));

注意:分配的空间为 10*sizeof(int), 因为malloc申请分配的空间是以字节为单位的。

int *p;
//通过指针变量p操作一块空间,可以存储4个int数据
//向系统申请 4*sizeof(int)字节的内存空间
p = (int *)malloc(4*sizeof(int)); //在堆上申请了4*sizeof(int)字节的内存空间
//p的值:申请到的堆上的内存空间的首地址 (指针p指向申请到的堆上的空间)
printf("p: %p\n", p);
//通过指针变量p 来操作申请到的堆空间
p[0] = 100;
p[1] = 200;
*(p+2) = 300;
*(p+3) = 400;

4、内存释放:free函数 

#include <stdlib.h>
void free(void *ptr);

功能:释放ptr所指向的内存空间

注意:
free函数并不会修改指针变量的值!但是free执行完成以后指针所指向的原来的那块地址空间中的内容是不确定的!!
问题:
释放空间到底做了什么事情呢?
最重要的是:告诉系统这块内存空间可以给别人使用了!!!

  • malloc 所在的源文件: #include <stdlib.h>
  • strcpy 所在的源文件: #include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]){
    char *p;
    //malloc分配的空间是在堆上的,需要手动释放
    p = (char *)malloc(10);
    strcpy(p, "hello");
    printf("p所指向的空间的内容: %s\n", p); //结果是hello

    //将p所指向的地址空间的首地址打印出来(就是将指针变量p的值打印出来)
    printf("p的值: %p\n", p);
    free(p);
    //将p所指向的地址空间的首地址打印出来
    printf("p的值: %p\n", p);

    //仔细观察,下面这条打印语句的结果
    printf("p所指向的空间的内容: %s\n", p);  //结果不是hello

    strcpy(p, "world");
    //再仔细观察,下面这条打印语句的结果
    printf("p所指向的空间的内容: %s\n", p);  //结果是world

    return 0;
}

free函数调用完以后的使用技巧: 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]){
    char *p;
    //malloc分配的空间是在堆上的,需要手动释放
    p = (char *)malloc(10);
    strcpy(p, "hello");
    
    //释放申请的内容
    free(p);
    p = NULL;
    return 0;
}

5、如果之前分配的空间不够了怎么办呢?
我们可以使用 realloc函数: 

#include <stdlib.h>
void *realloc(void *ptr, size_t size);

功能:在堆上分配一块size所指定的新的内存空间,空间大小单位为字节,并且还会将ptr所指向的空间中的内容拷贝到新的内存空间中,最后返回新的内存空间的额首地址
示例代码: 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
    char *p;
    p = (char *)malloc(10);
    strcpy(p, "hello"); //向分配的空间中拷贝字符串
    printf("p所指向空间的首地址: %p\n", p);
    printf("p所指向空间的内容: %s\n", p);
    p = (char *)realloc(p, 20); //重新分配新的空间
    printf("p所指向新的空间的首地址: %p\n", p);
    printf("p所指向新的空间的内容: %s\n", p);
    //注意:分配的新的空间的首地址有可能有之前分配的空间首地址一样,也有可能不一样
    strcat(p, " world"); //追加字符串
    printf("p所指向新的空间的内容: %s\n", p);
    return 0;
}

6、思考一种情景,char *dest, *src; 通过一个函数将src所指向的地址空间中的内容拷贝到dest所指向的地址空间中, 但是假设在调用函数前我们并不知道src的长度,这个时候我们需要将函数的形参设计为 二级指针

void test2(char **dest, char *src){
    //通过二级指针dest给形参一级指针dest分配内存空间!
    *dest = (char *)malloc(strlen(src) + 1);
    if (NULL == *dest || NULL == src)
        return ;
    strcpy(*dest, src);
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void func4(char **dst)
{
    //*dst == p
    *dst = (char *)malloc(10); //在堆上申请了10个字节
    strcpy(*dst, "hello");
}

int main()
{
    char *p; //指针指向某个函数调用结束后 在函数体中申请的堆空间的首地址
    /*
    * 既然我希望让p指向一块堆空间,其实就是希望对p进行赋值,赋值为在函数中申请的堆空间的首地址
    * 如何在函数中对p进行赋值呢?必须在调用函数的时候传递p的地址!!!!
    * */
    func4(&p);
    printf("%s\n", p);
    free(p);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/March_A/article/details/131333982