目录
一、基本概念
1.变量的存储空间
2.定义指针
3.引用与解引用
二、指针的算术运算、类型以及通用指针
1.指针的算数运算
2.指针类型以及通用型指针
三、指向指针的指针(pointers to pointers)
四、函数传值以及传引用
1.局部变量
2.从存储地址出修改局部变量
五、指针和数组以及数组作为函数参数
1.指针与数组
2.数组作为函数参数
六、指针和字符数组
1.创建一个字符数组
2.字符数组作为函数参数
3.字符数组的存储方式
一、基本概念
1.变量的存储空间
通常我们所说的内存是计算机的随机内存即RAM,并且不同的数据类型分配到的存储空间的大小也各不相同;整数类型数据 int 分配到的存储空间大小为 4bytes,字符类型的数据 char 分配到的存储空间为 1bytes,浮点数 float 分配到的存储空间为 4bytes。
2.定义指针
指针的本质就是一个整数类型的变量,用来存放被指向数据的地址,即 int *p = &a; 代表的是创建一个指针 p 用来存储变量 a 的地址,其中 & 代表的是取得数据 a 的地址。
#include<stdio.h>
int main()
{
int a = 3;
int *p = &a;
printf("%d\n", p);
return 0;
}
运行上面的代码得到的结果为:
即数据 a 的存储地址为 e21ff898。
3.引用与解引用
在定义指针时使用到的 * 表示的是引用,即得到被指向数据的地址,而在指针的前面使用 * 表示的是解引用,用来表示被指向数据的值。
#include<stdio.h>
int main()
{
int a = 3;
int *p = &a;
printf("%d\n", p);
printf("%d\n", *p); //输出p代表的地址的值,即a的值,为3
return 0;
}
运行上面的代码,前一行打印语句的输出为 a 的地址,由于第二行打印是对指针 p 进行反引用,所以第二行打印语句的输出为 a 的值 3。
二、指针的算术运算、类型以及通用指针
1.指针的算数运算
指针代表的是被指向数据的地址,因此指针的值加一或者减一,并不会使得被指向数据的值产生相应的加一或者减一的效果,而且指针加一或者减一,则指向的地址进行反引用得到的数据将会是乱码,而不是一个确切的值。
#include<stdio.h>
int main()
{
int a = 3;
int *p = &a;
printf("Address of a is : %d.\n", p);
printf("size of integer is: %d bytes.\n", sizeof(int));
printf("value of (p + 1) is : %d.\n", p + 1); //如果p = 2004,则 p+1 = 2008;这是因为整型的地址占用为4个字节
printf("value at address of p is : %d.\n", *p);
printf("value at address of (p + 1) is : %d.\n", *(p + 1)); //得到的是一个垃圾值
return 0;
}
运行上面的代码,得到的结果如下:
从上面的结果中可以看出,由于整型数据的存储大小为4个字节,因此对指针 p 进行加一的操作得到的数据是在原来的 p 的值上加了4,而不是单纯的加一操作。并且对 p+1 进行解引用得到的数值将会是一个垃圾值。
2.指针类型以及通用型指针
指针是强类型的(这是因为指针不止用来存储地址,也可以用它来解引用那些地址所对应的内容),也就是说不同的数据类型需要相对应类型的指针来进行指向;比如一个整型变量,指针指向的是第一个字节的位置,则计算机读取时向后再数三个字节得到该变量的值,若是使用其它类型的指针,则无法正确完成指向;虽然 float 类型的数据也分配有四个字节,但它和整型的数据并不相同,因此利用整型的指针并没有办法正确存储 float 类型的数据。
#include<stdio.h>
int main()
{
int a = 3;
int *p = &a;
float *p0 = &a;
printf("While int pointer is used, address of a is : %d, value of a is ; %d;\n", p, *p);//172 = 1010 1100
printf("While float pointer is used, address of a is : %d, value of a is ; %d;\n\n", p0, *p0); //可以看出两种类型的指针对应的地址是一样的,但值不一样
return 0;
}
运行上面的代码,得到的结果如下:
可以从结果中看出,对于整数类型的数据,使用浮点数类型的指针会得到错误的指向结果。
void 类型的指针,也被称为通用型指针,由于通用指针没有使用到任何类型的定义,因此不能对其进行反解。
#include<stdio.h>
int main()
{
int a = 3;
int *p = &a;
void *p0 = p; //通用指针没有使用到任何类型的定义,因此不能对其进行反解
printf("While int pointer is used, address of a is : %d, value of a is ; %d;\n", p, *p);//172 = 1010 1100
printf("While void pointer is used, address of a is : %d, value of a is ; %d;\n\n", p0, *p0); //可以看出两种类型的指针对应的地址是一样的,但值不一样
return 0;
}
运行上面的代码,则会发现程序报错:
通用型指针,不会指向任何类型的地址,故无法进行解引用。
三、指向指针的指针(pointers to pointers)
指针是用来表示被指向数据的地址的,故指针的本质是一个整型的变量,所以指向指针的指针的数据类型也是整型。
当定义了一个指针 int *p,则可以用 int **q 来存储指针 p 的地址,也可以使用 int ***r来存储指针 q 的地址,以此类推即可。
#include<stdio.h>
int main()
{
int a = 3;
int *p = &a;
printf("Address of data a is : %d.\n", p);
int **P = &p;
int ***q = &P;
printf("Address of pointer p is : %d.\n", P);
printf("Value of p(Address of a) is : %d.\n", *P);
printf("Value of a is : %d.\n", *(*P));
printf("Address of P is : %d.\n", q);
printf("Value of P(address of p) is : %d.\n", *q);
printf("Value of p(address of a) is : %d.\n", *(*q));
printf("Value of data a is : %d.\n", ***q);
***q = 10;
printf("a = %d\n", a);
**q = *p +2;
printf("%d\n", **q);
return 0;
}
运行上述代码得到的结果为:
在上面的代码中使用 p 来作为指针指向数据 a 的地址,P 作为指针来指向指针 p 的地址,q 作为指针来指向指针 P 的地址;通过解引用可以利用指针来改变被指向数据的值。
四、函数传值以及传引用
1.局部变量
当我在一个函数中声明一个变量,那么该变量称为局部变量,也就是说只能在该函数中对该变量进行修改以及处理。
#include<stdio.h>
int Increment(int a)
{
a = a + 1;
}
int main()
{
int a = 3;
Increment(a);
printf("a = %d.\n", a);
return 0;
}
运行上面的代码,若不考虑自定义函数 Increment() 中的局部变量的影响,则可以推测输出的 a 的值应该为 4。但通过运行代码得到的结果如下:
这说明在自定义函数中 a 是一个局部变量,和主函数中的 a 并不是同一个变量,也就是说在自定义函数中的变量存储的地址和主函数中的变量存储的地址是不一样的,所以我们需要从指针的层面对它们进行修改。
2.从存储地址出修改局部变量
由于局部变量和主函数中的变量的存储地址不相同,因此可以使用指针从变量的地址处进行处理。
#include<stdio.h>
void Increment(int *p)
{
*p = (*p) + 1;
}
int main()
{
int a = 3;
int *p = &a;
Increment(&a);
printf("a = %d.\n", a);
return 0;
}
运行上面的代码,可以实现利用自定义函数对主函数中的变量进行修改。
在自定义函数中的参数被称为形参,主函数中的参数称为实参,自定义函数的调用过程中实现了将实参转化到形参中进行处理的目的,这就是函数的调用。利用指针以及解引用进行处理的时候能够实现从参数的存储地址处修改参数而不是从参数的值处修改参数,从而达到处理数据的目的,这种方法被称为传引用。
五、指针和数组以及数组作为函数参数
1.指针与数组
定义一个数组后,使用指针指向该数组时,指针存储的是数组中的第一个元素的地址。数组名本身也可以用来代表一个地址,在使用指针时甚至可以不用取地址符,即可以写为 int *p = &A[i]或者 int *p = A + i。
#include<stdio.h>
int main()
{
int b[] = {0, 1, 2, 3, 4};
int *P = &b; //得到的是数组中的第一个元素的地址,在此处甚至可以不用对数组b取地址
int *P0 = &b[0]; //得到第一个元素的地址,此处也可以写成: int *P0 = b + 1;后面的写法类推
int *P1 = &b[1]; //得到第二个元素的地址,与第一个元素的地址相差4个字节
int *P2 = &b[2]; //与前一个元素的地址相差4个字节
int *P3 = &b[3]; //与前一个元素的地址相差4个字节
int *P4 = &b[4]; //与前一个元素的地址相差4个字节
printf("Address of P is : %d.\n", P);
printf("Address of P0 is : %d.\n", P0);
printf("Address of P1 is : %d.\n", P1);
printf("Address of P2 is : %d.\n", P2);
printf("Address of P3 is : %d.\n", P3);
printf("Address of P4 is : %d.\n", P4);
//利用该循环使用两种不同的方法输出数组及地址
for(int j = 0; j < 5; j++)
{
printf("%d:\n", j + 1);
printf("Address = %d\n", &b[j]); //直接取数组元素地址即可
printf("Value = %d\n", b[j]); //打印数组元素
printf("Address = %d\n", b + j); //数组名可以直接代表元素地址
printf("Value = %d\n", *(b + j)); //对数组名解引用得到数组元素的值
}
P0 = P0 + 1;
printf("%d\n", *P0); //输出的值为b[1]的值,即输出1
return 0;
}
运行上面的代码得到的输出为:
从运行结果中可以看出,数组名可以直接作为地址进行指针指向,且 *p = A + i 和 *p = A[i] 的运行效果是一样的。
2.数组作为函数参数
当数组作为一个参数时,编译器不会分配整个数组长度的空间用来进行存储,而是只分配出一个同名的指针的存储空间,即将 A 转化成为了 *A,并没有拷贝变量的值,而是直接拷贝了变量的地址,也就是说函数在传递变量的过程就是一个引用的过程。并且不同的操作系统中指针分配到的存储空间是不一样的,64位操作系统中占用了8个字节,32位操作系统中占用了4个字节。
尝试编写函数来计算数组的大小:
int SumofElements(int A[])
{
int i, sum = 0;
int size = sizeof(A)/sizeof(A[0]); //数组整体字节数目比上一个元素的字节数目
printf("SOE - Size of A = %d, size of A[0] = %d.\n",sizeof(A), sizeof(A[0]));
for(i = 0; i < size; i++)
{
sum += A[i];
}
return sum;
}
上面这个函数尝试以数组作为函数形参,并在函数中使用 for 循环来计算数组中的各个元素之和,但运行该自定义函数后发现计算结果是错误的。
#include<stdio.h>
int SumofElements(int A[])
{
int i, sum = 0;
int size = sizeof(A)/sizeof(A[0]); //数组整体字节数目比上一个元素的字节数目
printf("SOE - Size of A = %d, size of A[0] = %d.\n",sizeof(A), sizeof(A[0]));
for(i = 0; i < size; i++)
{
sum += A[i];
}
return sum;
}
int main()
{
//数组作为函数参数
int A[] = {1, 2, 3, 4, 5};
int size = sizeof(A)/sizeof(A[0]); //数组整体字节数目比上一个元素的字节数目
int total = SumofElements(A);
printf("Main - Size of A = %d, size of A[0] = %d.\n",sizeof(A), sizeof(A[0]));
printf("Sum of elements = %d.\n", total);
return 0;
}
从运行的结果中可以看出,该函数在计算数组中的元素之和时发生了错误,这是因为在作为函数参数时数组名被默认为是一个指针,而不是整个数组。
对自定义函数进行改写:
#include<stdio.h>
int SumofElements(int A[], int size)
{
int i, sum = 0;
for(i = 0; i < size; i++)
{
sum += A[i];
}
return sum;
}
int main()
{
int A[] = {1, 2, 3, 4, 5};
int size = sizeof(A)/sizeof(A[0]); //数组整体字节数目比上一个元素的字节数目
int total = SumofElements(A, size);
printf("Main - Size of A = %d, size of A[0] = %d.\n",sizeof(A), sizeof(A[0]));
printf("Sum of elements = %d.\n", total);
return 0;
}
运行得到的结果为:
可以看出该计算结果是正确的。
六、指针和字符数组
1.创建一个字符数组
在 C 语言中,创建一个字符数组需要的长度一般要比存储在该数组中的元素的个数多一,比如创建一个字符数组其中包含 john 这个字符串,那么可以将该字符数组的长度设置为5或者大于5即可,这是因为在字符数组中存储字符串之后,计算机并不知道字符串中的最后一个字母在哪一位上,故需要在最后一个元素的后面加上一个 NULL('\0')元素,用来指明字符串的结束,也就是说字符数组是以 '\0' 结尾的。
#include<stdio.h>
int main()
{
char C[4];
C[0] = 'j';
C[1] = 'o';
C[2] = 'h';
C[3] = 'n';
printf("%s", C);
return 0;
}
运行上面的代码得到的结果为:
可以看出,由于没有将字符数组的长度设置为大于字符串长度的值,打印出来的字符串没有一个合适的结尾。因此对代码进行修改:
#include<stdio.h>
int main()
{
char C[4];
C[0] = 'j';
C[1] = 'o';
C[2] = ‘h';
C[3] = 'n';
C[4] = '\0';
printf("%s", C);
return 0;
}
得到正确的结果为:
但是无论字符数组的长度设置为多大, 函数 sizeof() 都不会考虑字符串末尾添加的 NULL 元素,也就是说使用该函数得到的数组的长度均为其中的字符串的长度。
如果不想在定义字符数组的时候添加末尾的 NULL 元素,可以在定义字符数组的时候直接编写好其中需要增加的字符串,这样得到的字符数组进行打印不会出现错误的值。
#include<stdio.h>
int main()
{
char C[] = "john";
printf("%s", C);
return 0;
}
2.字符数组作为函数参数
当字符数组作为函数参数时,效果和一般的数组是一样的,也就是说字符数组在函数中也是被传引用,函数中传递的只是该数组的基地址(首个元素的地址),并不会将所有元素的地址在函数中进行传递。
#include<stdio.h>
int print(char *C)
{
int i = 0;
while(C[i] != '\0') //此处注意是单引号
{
printf("%c", C[i]);
i++;
}
printf("\n");
}
int main()
{
char C[20] = "HELLO"
char* A;
A = C;
print(A);
return 0;
}
运行上面的代码得到的结果为:
也可以将上面代码中的自定义函数进行修改,将其中的打印方式修改如下:
printf("%c", *(C + i));
效果和前面的是一样的,因为上面的修改是对数组指针进行了解引用。
还可以对自定义函数进行如下的修改:
int print(char *C)
{
while(*C != '\0')
{
printf("%c", *C);
C++;
}
printf("\n");
}
得到的结果和前面仍然是一样的。
3.字符数组的存储方式
#include<stdio.h>
int print(char *C)
{
while(*C != '\0')
{
printf("%c", *C);
C++;
}
printf("\n");
}
int main()
{
char C[20] = "HELLO";
print(C);
return 0;
}
上面的这部分代码和之前的一样,运行的时候得到的结果如下:
而且这部分代码可以在自定义函数中对字符串进行修改:
#include<stdio.h>
int print(char *C)
{
C[0] = 'A';
while(*C != '\0')
{
printf("%c", *C);
C++;
}
printf("\n");
}
int main()
{
char C[20] = "HELLO";
//char *C = "HELLO";
print(C);
return 0;
}
得到的运行结果为:
从结果中可以看出,在自定义函数中实现了对字符串的修改。
主函数中的字符串也可以使用其他的定义方式:
#include<stdio.h>
int print(char *C)
{
while(*C != '\0')
{
printf("%c", *C);
C++;
}
printf("\n");
}
int main()
{
char *C = "HELLO";
print(C);
return 0;
}
运行得到的结果为:
尝试在自定义函数中对字符串进行修改:
#include<stdio.h>
int print(char *C)
{
C[0] = 'A';
while(*C != '\0')
{
printf("%c", *C);
C++;
}
printf("\n");
}
int main()
{
char *C = "HELLO";
print(C);
return 0;
}
运行上述代码会发现程序无法输出:
也就是说使用下面的定义字符串的方式时,无法在自定义函数中对字符串进行修改。
这是因为,当使用上面的方式定义字符串时,是直接将整个数组存储在一起,因此可以对其中的一两个元素进行修改;但当使用下面的方式定义字符串时,时将字符串的地址作为变量进行传输,没办法在自定义函数中对其中的某个元素进行修改。
到这里算是总结完指针中的部分内容了,还有一大堆需要学习的东西,真的是非常的棘手。