指针概念

指针是C语言的灵魂,C语言中的指针极其灵活,正是因为C语言中有指针,C语言才能在最近40年经久不衰。

地址

机器程序将存储器看作一个非常大的字节数组,每个字节都由一个唯一的数字标识。这个标识就是该存储单元的地址(address)。所有可能的地址的集合称为地址空间(address space)。

比如物理内存,每个物理内存单元都有一个编号,这个编号就是物理地址,所有的物理地址的集合就叫做物理地址空间。

虚拟地址空间在内存管理章节再详细讲解。

指针变量

  • 指针变量也是一种变量。

    指针变量本质上和普通变量是一样的,普通变量存储的是数据,指针变量存储的也是数据只不过这个数据是地址

  • 指针存放的内容是一个地址,该地址指向一块内存空间。 如果指针变量p存储的是变量a的地址,那么我就可以称p是指向变量a的指针。

    int *p 表示定义一个指针变量 int *p = &a; p表示指针所指向变量变量a在内存中所处的位置。

  • 指针类型也是一种数据类型。

指针变量的sizeof运算

32位平台下, 任意指针类型的变量都是占4个字节。64位平台下, 任意指针类型的变量都是占8个字节。 sizeof(int) == sizeof(char) == sizeof(double*)

*解引用运算符

*p 代表指针所指内存空间中所存的数据内容。


int num = 10;
int *p = #

printf("*p = %d\n", *p); // *p 解引用
*p = 100;

注意: 指针变量只能存放地址,不能将一个int型的变量直接赋值给指针变量。如:int *p = 100;

&取地址运算符

&可以获取一个变量在内存中的地址。

int a  = 100;
int *p = &a;

那么&a表示的是变量a在内存中地址;&p表示的是指针变量p在内存中所处的地址。

注意: register int a;中,a是一个寄存器变量。一旦register修饰一个变量,无论这个变量最后有没有保存到寄存器中,都不能对它取地址操作。

void*指针

定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将void *转化为其他类型指针,也可以用(void *)将其他类型指针强制转化为void类型指针。 void *p,指针之间赋值一般需要类型兼容,但任何类型的指针都可以赋值给void *

NULL和野指针

  • NULL在C语言中的定义为(void *)0 空指针就是指向了NULL的指针变量。

  • 野指针,如果声明一个指针变量,没有初始化,那么这个指针变量的值是垃圾值,可能指向了一块随机的空间。这块空间可能是没有使用,也可能被别的代码使用,也可能是系统占用。去访问指针指向的空间的时候,如果是系统占用的空间,就会报错. 这样的指针就叫做野指针。所以在代码中要避免出现野指针。

  • 如果一个指针不能确定指向任何一个变量的地址,那么就将这个指针赋值为NULL防止不可预期的访问。

指针的兼容性

指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个double *赋值给int *。 虽然可以强制转换指针类型,但是不清楚强制类型转换的用途,不建议使用强制转换。

指向常量的指针和常量指针

参考资料C++ primer(第5版 P56)

const char *p;定义一个指向常量的指针(pointer to const),即p是一个指针常量。 其实更加严谨的说应该是 不能通过指针常量修改所指向对象的值。这样造成的现象就是好像指向的对象是一个常量。

如果希望一个变量不能通过指针修改,就使用const char *p;

char *const p;定义一个常量指针(const pointer),一旦初始化之后其内容不可改变。 如果希望可以通过指针修改变量,但是要限定指针p不能再指向其他变量,就使用char *const p;

Bjarne在他的The C++ Programming Language里面给出过一个助记的方法: 把一个声明从右向左读

char * const cp; ( 解引用符号 读成 pointer to ) cp is a const pointer to char

const char * p; p is a pointer to const char;

注意:c语言中的const不够严谨,因为可以通过指针变量间接的修改局部const常量的值,所以在c语言中用#define常量的时候更多。

数组和指针

int main(void)
{

    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int *p = arr;

    p[3] = 20;
    int i;

    // 法1
    for (i = 0; i < 10; i++) 
    {
        printf("p[%d] = %d\n",i,p[i]);  // 当指针指向数组,那么指针的变量名就可以当做数组名来用
    }
    // 法2
    for (i = 0; i < 10; i++, p++) 
    {
        printf("p[%d] = %d\n",i, *p);
    }
    return 0;
}
// 指针可以指向数组任意一个元素.
int main(void)
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};

    int *p = &arr[2]; // p[0] 初始值为arr下标为2的元素

    p[3] = 20; // ---> 等价于 arr[5] = 20;
    int i;
    for (i = 0; i < 10; i++) 
    {
        printf("p[%d] = %d\n",i,arr[i]);
    }

    return 0;
}

指针运算

指针的偏移(步长)

  • 指针 + 1; ---> 在这个指针地址基础上加上"该指针指向的变量数据类型"占用的字节数。 int *p = # p+1; // (long int)p + 4 int类型

char ch = 'a'; char *cp = &ch; cp + 1; // (long int)cp + 1

  • 指针 - 1; ---> 在这个指针地址基础上减去"该指针数据类型"占用的字节数。 同理。

  • 指针是int* 1代表4个字节地址

  • 指针是float* 1代表4个字节地址
  • 指针是double* 1代表8个字节地址
  • 指针是char* 1代表1个字节地址

指针的取址能力

  • 不同指针变量的本质

指针操作数据的时候,一次操作几个字节,是由指针的数据类型来决定的,这就是指针的取址能力。

拓展

  • 中括弧的本质
p是指向数组的指针,
p[n] == *(p+n)

所以,刚才的程序可以理解为:

int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};

    int *p = &arr[2]; //p = arr + 2 --->  p[0] 初始值为arr下标为2的元素

    p[3] = 20; // p[3] = *(p+3) = *(arr+5) ---> 等价于 arr[5] = 20;
    int i;
    for (i = 0; i < 10; i++) {
        printf("p[%d] = %d\n",i,arr[i]);
    }

    return 0;
}

指针数组

存储多个指针的数组就是指针数组。

int *p[5];

数组指针

  1. 演示 arr , &arr的区别。
int arr[5] = {1,2,3,4,5};

printf("%p --- %p\n", arr, &arr);

arr: 是数组连续空间的标识,数组类型是int [5],所以sizeof可以获取整个数组的空间大小。 当他被当做地址使用时,就会隐式转换为首元素的地址。

&arr: 数组的地址。数组的类型是int [5],所以指向这个类型的指针类型是int (*)[5] 即: int (*p)[5] = &arr;

2.数组指针的定义

数组指针是一个指向数组的指针。 int (*p)[5];

赋值:

int arr[5] = {1,2,3,4,5};
int (*p)[5] = &arr;

3.数组指针的使用

int arr[5] = {1,2,3,4,5};
int (*p)[5] = &arr;


(*p)[0] = 10;
// p[0] = *(p + 0) = *p
// p[0][0]  p[0][1]

4.指向二维数组的数组指针


int arr[6][5] = {1,2,3,4,5};

int (*p)[5] = arr;

for(int i = 0; i < 6; i++ )
{
    for(int j = 0; j < 5; j++ )
    {
        printf("p[%d][%d] = %d\n",i,j, p[i][j]); // 等价于 arr[i][j]

    }

}

应用场景:二维数组传参

二级指针

指针就是变量,既然是变量,就有地址,所以可以定义一个指针来指向另一个指针。

二级指针只能存储一级指针的地址。

n级指针只能存储n-1级指针的地址。

int main()
{
    int num = 10;
    int* p1 = &num;

    // 二级指针
    int** p2 = &p1; // 存储的是一级指针变量p1的地址
    // int** p2 = &num; 直接报警告
    printf("&num = %p\n",&num);
    printf("p1 = %p\n\n",p1);

    printf("&p1 = %p\n",&p1);
    printf("p2 = %p\n",p2);
    return 0;
}

指针变量作为函数的参数

函数的参数可以是指针类型,它的作用是:将一个变量的地址传送给另一个函数。

void test(int *p);//定义一个函数,形参是int *类型

注意: C语言中如果想通过函数内部修改外部实参的值,那么就需要给函数的参数传递这个实参的地址。

数组名作为函数的参数

当数组名作为函数的参数,C语言将数组名解释为指针。

如:

int func(int array[10]); // array是一个int *
int func(int array[]);
int func(int *array);
  • 如果数组名做为函数的参数,那么这个就不是数组名了,而是一个指针变量名.

    验证方式就是在数组中查看 sizeof(array)的大小

  • 当把一个数组名做为函数的参数时候,修改形参的值,同时影响实参的数组成员的值

  • 如果把一个数组名做为函数的参数,那么在函数内部就不知道这个数组的元素个数了,需要再增加一个参数来标明这个数组的大小.

const关键字保护数组内容

如果将一个数组做为函数的形参传递,那么数组内容可以在被调用函数内部修改,有时候不希望这样的事情发生,所以要对形参采用const参数。

func(const int array[]);

指针作为函数的返回值

指针也可以作为函数的返回值返回。

char* test();

但是,如果一个函数返回指针,那么就要保证在函数结束后,这个指针还能访问其指向的内存。否则就会导致错误。

memset,memcpy,memmove函数

*这三个函数分别实现内存设置,内存拷贝和内存移动。

**使用memcpy的时候,一定要确保内存没有重叠区域。**
  • 使用时需要包含头文件#include
void *memset(void *s, int c, size_t n);

设置一块内存区域,第一个参数是内存首地址,第二个参数是要设置的值,第三个参数是这块内存的大小,单位:字节 memset主要的功能就是把一块内存设置为0。

void *memcpy(void *dest, const void *src, size_t n);//内存拷贝

第一个参数是目标内存首地址,第二个参数是源内存首地址,第三个参数是拷贝字节数。前提是不能有内存重叠区。

void *memmove(void *dest, const void *src, size_t n);
//内存移动,参数与memcpy一致 主要用于在有内存重叠区的两个空间的数据拷贝

字符指针和字符串

指针和字符串

一般字符串的操作,其实是指针操作.

char s[] = "hello world";
char *p = s;
p[0] = 'a';

通过指针访问字符串

int main()
{
    char s[] = "abcde";
    char *p = s;
    p[0] = '1';
    printf("%s\n", p);

    return 0;
}

两种字符串的区别

  1. 相同点
char s[] = "hello world";
char *p = "hello world";

printf("%s\n", s);
printf("%s\n", p);
  1. 不同点

字符数组保存的字符串可以修改,字符串的每个字符都保存在栈区。

字符指针保存的字符串 存储在常量区,不能修改。

char *p = "hello world";
printf("%s\n", p);

p = "how are you"; // 不是直接修改字符串内容,而是保存新的字符串到常量区,返回新的地址。

printf("%s\n", p);

函数的参数为char *

  • char指针作为函数的参数,可以在函数内部修改字符数组的值.

  • 如果不希望函数内部通过指针修改字符数组,那么可以用const修饰.

  • char指针作为函数参数时,在函数内部是可以获取字符串有效字符的数量,所以不需要额外传递字符串的长度了。

指针数组作为函数的形参

指针的偏移

更加变态的指针运算规则

通常在C语言中仅当指针具有显式类型的时候才可用于计算。例如,int或 long 。 否则不可能确定指针加1操作的语义,GNU编译器拓展了该限制,支持void类型的指针和函数指针的运算,在Linux内核中很多地方都有用到。这两种情况下加1的语义是增加一个字节。

有趣的是 GCC曾经支持过对bit寻址的体系结构,即TI(texas Instrument,德州仪器)的34010处理器。指针加1在该机器上意味着内存地址向前移动1bit位,而不是直接的移动一个字节。 更有趣的是 2.6系列内核开发的关键人物 Andrew Morton曾经为该处理器编写过一个实时内核,可以从www.zip.com/au/~akpm/下载源代码。

char*可能跟你想的不太一样

char ,它在C/C++中有专门的语义,既不同于signed char ,也不同于unsigned char ,专门用于指以'\0'为结束的字符串 在C++中,你可以试一试,用char p="abcd"; 是可以通过编译的 但不论用 signed char *p="abcd"; 还是 unsigned char *p="abcd"; 都会有warning。

关于这些在C/C++的语言标准或一些经典书籍如《The C++ Programing Language》中都有很清楚的说明。

判断编译器的默认char符号

#include <stdio.h>
int main(void)
{
    char c=-1;
    if(c<200)
    {
        printf("signed\n");
    }
    else
    {
        printf("unsigned\n");
    }
    return 0;
}

课堂练习 ----题目待分类

  1. 指针作为函数参数

  2. 字符串逆置

作业

  1. 编写一个函数比较两个字符串的长度谁长(不使用库函数)。

  2. 问题代码纠错 int str_len_cmp(const char s1,const char s2) { return strlen(s1) - strlen(s2); }

说说这个函数实现有没有问题?为什么?如果有,怎么解决

  • 简述一下指针的本质 ,并且说明下面每句代码的意义 , 并加上初始化的语句他们有初值。
    char  *ptr;
    char  arr[32];
    char *ptr[32];
    char (*ptr)[32];
    
  • 下面程序的输出结果是( )

    include

    int main( ) { int a[]={1,2,3,4,5,6,7,8,9,0},p; p=a; printf(“%d”,p+9); }

A)0 B)1 C)10 D)9

  • 请编写1个函数. 该函数返回1个整型数组中的最大值、最小值、平均值、累积和. 提示:利用指针返回多个数据.
  • 已知一个整型数组int arr[10] = {10,34,29,3,14,55,16,37,8,9}; 1>请定义一个指针,接收这个数组第一个元素的地址并且利用指针的加法运算遍历这个数组 2>请在遍历的过程中,把所有元素修改成100.
  • 定义一个int *p的指针,已知int a = 10;将a的地址赋值给p的写法正确的是

A: p = a; B: p = &NULL; C: *p = NULL; D: p = &a;

  • 以下程序中调用scanf函数给变量a输入数值的方法是错误的,其错误原因是?
#include<stdio.h>
int main(void)
{
    int *p, *q, a, b;
    p=&a;
    printf("input a:");
    scanf("%d", *p);
    …
}

A: p表示的是指针变量p的地址 B: p表示的是变量a的值,而不是变量a的地址 C: p表示的是指针变量p的值 D: p只能用来说明p是一个指针变量

  • 有代码如下:

char *p1;

请问p1指针指向了内存中的那块空间?

  • 有代码如下:

int num = 500;

char *p1 = #

*p1 = 100;

请问num的值是多少?

  • 有代码如下:

int num = 10;

int *p1 = #

请写代码输出num、p1 变量的地址和值.

  • 有1个数组:

int arr[] = {10,20,30,40,50,60,70,80,90,100};

请使用四种方式遍历方式将这个数组遍历出来.