C语言基础学习---指针

13. 指针

13.1 指针四个方面

理解指针,需要弄清楚下面四个方面

  1. 指针的类型
  2. 指针所指向的类型
  3. 指针的值或者叫指针所指向的内存区域
  4. 指针本身所占据的内存区
1
2
3
4
5
(1)int*ptr;
(2)char*ptr;
(3)int**ptr;
(4)int(*ptr)[3];
(5)int*(*ptr)[4];

13.1.1 指针的类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。

1
2
3
4
5
(1)int*ptr;// 指针的类型是 int*
(2)char*ptr;// 指针的类型是 char*
(3)int**ptr;// 指针的类型是 int**
(4)int(*ptr)[3];// 指针的类型是 int(*)[3]
(5)int*(*ptr)[4];// 指针的类型是 int*(*)[4]

13.1.2 指针所指向的类型

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符 * 去掉,剩下的就是指针所指向的类型。

1
2
3
4
5
(1)int*ptr; // 指针所指向的类型是 int
(2)char*ptr; // 指针所指向的的类型是 char
(3)int**ptr; // 指针所指向的的类型是 int*
(4)int(*ptr)[3]; // 指针所指向的的类型是 int()[3]
(5)int*(*ptr)[4]; // 指针所指向的的类型是 int*()[4]

13.1.3 指针的值「指针所指向的内存区域的地址」

指针的值是指指针本身存储的数值,这个值被编译器当做一个地址,而不是一般的值。在64bit机器上,所有类型的指针的大小都是 8 Byte。

  • 指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为 sizeof(指针所指向的类型) 的一片区域。
  • 一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域。
  • 一个指针指向了某一块内存区域,就相当于说该指针的值是这块内存区域的首地址。

13.1.4 指针本身所占据的内存区

使用 sizeof 函数测量一下就知道了,在 32 bit 机器中,指针本身占据 4 个字节,64 bit 机器中,指针本身占据 8 个字节。

13.2 指针的算术运算

  1. 指针加上整数

    指针p加上整数j产生的是指向特定元素的指针。这个特定元素是p原先指向的元素后的j个位置,更确切说,如果p指向数组元素a[i],那么 p+j 指向a[i+j];

  2. 指针减去整数

    和指针加上整数类似

  3. 两指针相减

    两指针相减,结果为指针之间的距离,用数组元素的个数来度量,如果p指向a[i],q指向a[j],那么 p -q 等于 i -j 。只有两个指针指向同一个数组时,他们的相减才是有意义的。

  4. 指针比较

    使用 < ,<=, >, >=,==,!= 进行指针比较,两指针指向同一数组时,比较才有意义。比较结果依赖于两指针的相对位置。

    1
    2
    3
    p = &a[5];
    q = &a[1];
    //p<=q 结果为0,p>=q结果为1

13.3 指针用于数组处理

运算符优先级 后缀++ > 前缀++ > *

把值存入到一个数组元素中,然后前进到下一个元素。

  • 用数组下标的写法

    a[i++] = j;

  • 如果 p 指向数组元素,那么对应的写法是

    *p++ = j;

    这里因为后缀++的优先级高于*,所以编译器将这一条语句看成

    *(p++) = j;

    p++的值是p,因为使用后缀++,所以p只有在表达式计算出来后,才可以自增,因此(p++) 的值是 p。

  • 各种表达式和含义

    p++ 或 (p++) ,先取 *p 值,然后指针 p++

    (p)++ , 先取 p 值 v, 将 v 值加一,p不变。

    ++p 或 (++p), 先将指针p ++,然后取 ++ 之后的指针指向的地址的值

    ++ p 或 ++( p) ,先取 *p 值 v,然后将 v 值加一,p 不变

  • 指针是直接对内存进行操作,效率高,但是如果操作不当,存在十分巨大的风险,java 当中就省略了指针。

  • 数组名就是数组首元素的地址, 这就话是错误的。数组名 array 代表的是整个数组空间,就像 int i = 8; 一样,整型变量 i 在内存中是占有 4 个字节的「64bit」空间,这个空间中保存的值是4。而 int[] 型变量 array 在内存中占有 4 * 10 =40 个字节的空间,它的地址是这 40 个字节空间的首地址,而这 40 个字节的空间的首地址刚好就是数组第一个元素单元的首地址。

  • 数组名 array 代表的就是这整个数组空间的地址,所以可以用将它赋值给指针,因为指针就是地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(){
int array[10]={0};
int i = 6;
printf("array sizeof = %lu \n", sizeof(array));
printf("i = %p \n", i);//warning
printf("&array = %p, &array[0] = %p, &array[1] = %p", &array, &array[0], &array[1]);
/*输出
array sizeof = 40
array = 0x7ffee7e49990
i = 0x6
&array = 0x7ffee7e49990, &array[0] = 0x7ffee7e49990, &array[1] = 0x7ffee7e49994%
*/

}
  • 数组的下标访问方式会被转换为指针访问方式,后者效率更高

13.4 用数组名作为指针

可以使用数组的名字作为指向数组第一个元素的指针

1
2
int a[10]={12345678910};
//这里*a = 1, *(a+1) = 2;

a + i 等同于 &a[i] ,两者都表示的是指向数组 a 中第 i 个元素的指针。并且 (a + i) 等价于 a[i],因为 & 这两个运算符是可以抵消,可以理解为两者互为逆运算,就像「取对数」和「取指数」。

相应的,可以以吧指针看做数组名,进行取下标操作

1
2
3
4
5
6
#define N 100
int a[N],i,sum=0,*p=a;
for(i=0;i<N;i++){
sum+=p[i];
}
//编译器吧 p[i]看做 *(p + i), 这时指针算术运算非常正规的用法。

13.5 指针与字符串

13.5.1 字符指针与字符数组

1
2
char date[] = "hello world";
char *date = "hello wordl";
  • 前者 date 是一个数组,后者 date 是一个指针。

  • 在声明为数组时,就像任意数组元素一样,可以修改存储在 date 中的字符;在声明为指针时,date 指向的是字符串字面量,它是不可以修改的,它存储在静态区当中。

  • 在声明为数组时,date 是数组名,声明为指针时,date是变量,这个变量可以在程序执行期间指向其他的字符串。

13.5.2 搜索字符结尾

string.h 文件中的 strlen 用于获取字符串长度,它会从字符串首地址开始计数,直到遇到第一个 ‘\0’ 为止。那么这里就会涉及到搜索一个字符串中的结尾。

1
2
3
4
5
6
7
seize_t strlen(const char *s){
size_t n = 0;
for(n = 0; *s != '\0'; s++){
n++;
}
return;
}

精简版,注意 s != ‘\0’ 和 s != 0; 是一样的,因为空字符的整数值就是0;

1
2
3
4
5
6
7
8
seize_t strlen(const char *s){
size_t n = 0;
while(*s++){
n++;
}
return n;

}

注意,因为 *s++ 是先计算表达式,然后再讲 s++ ,所以这里 s 最后停下来的位置是 ‘\n’ 后面的一位。

上面的简化没有提高它的运行速度。下面再简化

1
2
3
4
5
6
7
seize_t strlen(const char *s){
const char *p = s;
while(*s){
s++;
}
return s -p;
}

这里用空字符的地址减去字符串中第一个字符的地址。速度的提升在于不需要再while循环内部对n进行自增操作。

总结下来,搜索字符串结尾的空字符有两种惯用法,

1
2
3
4
5
while(*s)
s++; // s 最终指向空字符位置

while(*s++)
; //s 最终指向空字符的下一个位置

13.5.3 拼接字符串

也就是 string.h 当中的 strcat 函数。将字符串 s2 拼接到 s1 的末尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *my_strcat2(char *s1, const char *s2){
char *p = s1;
while(*p != 0){
p++;
}
while(*s2 != 0){
*p = *s2;
p++;
s2++;
}

*p = '\0';
return s1;
}
  1. 确定字符串s1莫问空字符串的位置,并且使用指针指向它

  1. 把字符串s2中的字符逐个复制到p所指向的位置

  1. 第二个while循环在 s2 指向空字符时候,就停止了,并且此时p指向s1 拼接后的最后一个字符的后面一位,也就是新的字符串的空字符处,所以还需要额外的添加空字符。

这里还有简化写法

1
2
3
4
5
6
7
8
9
10
char *my_strcat1 (char *s1, const char *s2){
char *p = s1;

while(*p) //搜索字符串结尾的空字符
p++;
while(((*p++) = (*s2++))) //开始拼接
;
return s1;

}

第二个 while 循环表达式的意思是,将 s2 指向的值,赋给 s1 p 指向的值,然后 p 和 s2 各自 ++,并且while 语句会测试复制表达式的值,也就是测试复制的字符,除空字符以外,所有的字符的测试结果都为真,所以,循环只有在复制空字符后才会终止,而且由于循环是在赋值空字符之后终止,所以不需要单独用一条语句在新的字符串的末尾添加空字符。

13.6 数组型参数

函数参数为数组,实际上实参传递过去的是数组首元素的地址,也就是指针,在 64bit 机器上,指针的大小为 8 个字节,下面程序的输出可以证实。

1
2
3
4
5
6
7
8
9
10
11
12
void show_array(int array[]){
cout<<"show_array, array sizeof : "<<sizeof(array)<<endl;
}
int main(){
int array[10]={0};
int i = 6;
int *p;
cout<<"pointer size: " <<sizeof(p)<<endl; // 8
cout<<"array sizeof : "<<sizeof(array)<<endl; //40
show_array(array); //8
return 0;
}
  • 在给函数传递普通变量时,变量的值会被复制,任何对形式参数的改变都不会影响到变量。但是如果传递的是数组,那么相当于传递的是数组首元素的地址,对形参的操作是可以改变数组的内容的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    void show_array(int array[]){
    int *p;
    for(p = array; p < array + 10; p++){
    (*p)++;
    }
    }


    int main(){
    int array[10]={11,22,33,44,55,66,77,88,99,1010};
    show_array(array);
    int *p;
    for(p = array; p < array + 10; p++){

    printf(" %d",*p);
    }

    printf("\n");
    return 0;
    //输出 12 23 34 45 56 67 78 89 100 1011
    }
  • 给函数传递数组所需要的时间与数组大小无关,因为没有对数组进行复制

  • 可以把函数的数组型参数申明为指针,编译器将这两种声明看做是完全一样的。「对形式参数而言,两者是一样的,但是对变量而言,声明为数组和指针是不同的 inta[10] 编译器预留 10 个整数的空间 64bit 机器 40 字节,int *p 编译器预留指针类型变量的空间 64bit 机器 8字节。

  • 用 const 标识参数指针,表明函数不会改变指针参数所指向的对象「但是可以改变指针本身」,const 放在形式参数的声明之中,后面紧跟形式参数的类型说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    void f1(const int *p){
    int j;
    *p = 0; //非法
    p = &j; //合法

    }

    vois f2(int * const p){
    int j;
    *p = 0;//合法 p = &j;//非法
    }

    vois f3(const int * const p){
    int j;
    *p = 0;//非法
    p = &j;//非法
    }

13.7 指针和多维数组

首先明确,c语言按行主序存储二维数组,如果一个指针p指向 a [0] [0],然后一直进行p++操作,指针会移动 a [0] [1],a [0] [2],a [0] [3],到了第一行最后一个元素后,会指向 a [1] [0]。 并且 c 的编译器是将二维数组看做一维数组的,只是一维数组的内容也是一个数组。

13.7.1 处理多维数组的行

1
2
3
p = &a[i][0]; //表示指针p指向二维数组的第i行第1个元素
//由于对于任意的二维数组a来说,a[i] 表示的就是第i行中的低一个元素的指针,那么上面可以简化为
p = a[i];

现在需要的对二维数组的第 i 行清零。

1
2
3
4
5
//这里的指针p指向的是一个整数,是从第i行的第1个元素到最后一个元素
int a[NUM_ROWS][NUM_COLS],*p,i;
for(p = a[i];p < a[i] + NUM_COLS; p++){
*p = 0;
}

13.7.2 处理多维数组的列

将二维数组的第 i 列置为0;

1
2
3
4
5
//这里的指针p指向的a这个二维数组的第一个元素,也就是一个一维数组,因为二维数组可以看做存储的类型为数组的数组
int a[NUM_ROWS][NUM_COLS],(*p)[NUM_COLS],i;
for(p = &a[0]; p < &a[NUM_ROWS]; p++){
(*p)[i] = 0;
}

先明确几个要素

  • int (p)[NUM_ROWS] 表示的是一个指针p, 这个指针指向一个数组,数组长度为 NUM_ROWS,数组中元素的类型为int。这里() 必须有。指针的类型是 int( )[NUM_ROWS], 指针锁指向的类型为 int() [NUM_ROWS]
  • (*p)[i] = 0 表示的是将 p 指向的这个数组的第 i 个元素赋值为0,这里() 必须有。

13.8 字符串数组

一般字符串是使用字符数组的形似来存储,那么字符串数组,字面意义理解就是字符数组的数组,也就是一个二维数组。

1
char planets[][8]={"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"}

这个二维数组的结构图如下:

并非所有额字符串都足以填满数组的一行,所以c用空字符来填补,这样对空间是很大的浪费。

大部分的字符串集都是长字符串和段字符串的混合。应该这样写:

1
char *planets[]={"Mercury","Venus","Earth","Mars","Jupiter","Saturn","Uranus","Neptune","Pluto"}

对于 planets ,它的类型是 char *[], 它指向的类型是 char[] ,所以,planets 是一个数组,数组中存储的元素是指针,指针指向的类型是字符。

它的存储结构如下,这样字符串中不再有任何浪费的字符。

13.9 指针与函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

int max(int a, int b){
printf("max, a = %d, b = %d \n",a, b);
return a>b?a:b;
}

int min(int a, int b){
printf("min, a = %d, b = %d \n",a, b);
return a>b?b:a;
}

//int(*pfun)(int, int) 有点像java中的多态起的作用,兼容有多种固定格式的函数
int exe(int a, int b, int(*pfun)(int, int)){
return pfun(a,b);
}


int main(){

int a = 5, b = 50;
//max函数名可以表示这个函数的内存空间的首地址
printf("max %p\n",&max);

exe(a,b,max);

return 0;
}

13.9 复杂指针类型

1
2
3
4
5
6
7
8
9
int p; //这是一个普通的整型变量
int *p; //首先从P 处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针
int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组
int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与*结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组
int (*p)[3]; //首先从P 处开始,先与*结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针
int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.由于二级指针以及更高级的指针极少用在复杂的类型中,所以后面更复杂的类型我们就不考虑多级指针了,最多只考虑一级指针.
int p(int); //从P 处起,先与()结合,说明P 是一个函数,然后进入()里分析,说明该函数有一个整型变量的参数,然后再与外面的int 结合,说明函数的返回值是一个整型数据
Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的*结合,说明函数返回的是一个指针,,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与*结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.
共字
0%
.gt-container a{border-bottom: none;}