一、一级指针
指针也是一个变量,指针存放的内容是一个内存地址,该地址指向一块内存空间。
1.1 指针变量的定义
一级指针变量的数据类型会在基本数据了类型之后多了一个* 号,指针变量只能存放内存地址(一个16进制的数),不能将一个基本数据类型直接赋值给一个指针变量。
如果要取出一级指针变量指向的内存地址所对应的值的话,可以通过在指针变量前加一个* 号来获取
int *p;
*p;
int *p = 100;
*p =100 ;
1.2 &取地址运算符
通过& 符号可以取得一个变量在内存当中的地址,然后就可以赋值给指针变量了。
#include<stdio.h>
int main()
{
int a = 100;
int *p;
p = &a;
*p = 200;
printf("p=%p,*p=%d,a=%d\n",p,*p,a);
a = 50;
printf("p=%p,*p=%d,a=%d\n",p,*p,a);
return 0;
}
两次输出结果如下: 可以看到*p和a变量的值是一样的,并且修改值并不会影响p指针变量指向的内存地址。
1.3 无类型指针
定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将void *转化为其他类型指针,也可以用(void *)将其他类型指针强制转化为void类型指针。 void *p 他可以指向任意类型的内存地址
1.4 空指针与野指针
NULL在C语言中的定义为(void *)0 ,它是一个宏常量, #define NULL 0 .如果一个指针变量没有明确指向一块内存地址, 那么就把这个变量指向NULL,这个指针就是空指针,空指针是合法的,例如:
int *p = NULL;
int *p;
p = NULL;
指向NULL的指针叫空指针,没有具体指向任何变量地址的指针(也就是没有初始化值的指针)叫野指针。
#include<stdio.h>
int main()
{
int *p;
*p = 100;
return 0 ;
}
1.5 指针的兼容性
指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个double * 赋值给int * ,它是不会自动类型转换的. 原则上一定是相同类型的指针指向相同类型的变量地址,不能用一种类型的指针指向另一种类型的变量地址。
1.6 常量指针与指针常量
1、指向常量的指针(常量指针) 常量指针的const书写在指针类型之前,例如: const char *p; 定义一个指向常量的指针, 但是这个指针不能通过*p 的方式取修改变量的值了。但是可以通过*p的方式读取变量的值。
#include<stdio.h>
int main()
{
int a = 10;
const int *p = &a;
*p = 100;
printf("a=%d\n",*p);
return 0;
}
2、指针常量 指针常量的const书写在指针类型之后,例如: char * const p; 定义一个指针常量,一旦初始化之后其内容不可改变,也就是说不能再指向其他变量的地址了。但是可以通过*p的方式改变变量的值。
#include<stdio.h>
int main()
{
int a = 10;
int *const p = &a;
int b = 20;
p = (int *)&b;
printf("%d\n",*p);
return 0;
}
原则上在C语言中常量指针的常量不能修改、指针常量的指针不能修改;但是C语言中通过const定义的常量是有问题的,因为可以通过指针变量来间接的修改常量的值,所以在C语言中用#define常量比较多,而C++的const是没办法改的。
1.7 指针与数组的关系
一个变量有地址,一个数组包含若干个元素,每个元素在内存中都有地址,而且是连续的内存地址.也就是说如果数组是char类型的,那么每个元素的内存地址就是相差1, 如果是int类型的数组, 那么元素间的内存地址是相差4,例如:
int a[10];
int *p = a;
p和&a[0]的地址,以及a数组名的地址都是一样的。 也就是说数组名的地址等于数组首元素的内存地址。
#include<stdio.h>
int main()
{
char a[10];
printf("a=%p\na[0]=%p\na[1]=%p\na[2]=%p\n",a,&a[0],&a[1],&a[2]);
printf("========================\n");
int b[10];
printf("b=%p\nb[0]=%p\nb[1]=%p\nb[2]=%p\n",b,&b[0],&b[1],&b[2]);
return 0;
}
输出结果如下:
a=0x16efa760e
a[0]=0x16efa760e
a[1]=0x16efa760f
a[2]=0x16efa7610
========================
b=0x16efa75e4
b[0]=0x16efa75e4
b[1]=0x16efa75e8
b[2]=0x16efa75ec
从结果也可以看出char数组的每个元素内存地址相差1, int数组的元素内存地址相差4.
注意:当指针变量指向一个数组名的时候,C语言语法规定指针变量名可以当做数组名使用.但是指针变量的sizeof不是数组的sizeof, 指针变量的sizeof在64位系统是8个字节, 32位系统是4个字节.
#include<stdio.h>
int main() {
int a[] = {1, 2, 3, 4, 5};
int *p;
p = a;
p[3] = 100;
printf("数组长度=%lu字节,指针变量长度=%lu字节\n", sizeof(a), sizeof(p));
int i;
for (i = 0; i < 5; i++) {
printf("a[%d]=%d\n", i, p[i]);
}
return 0;
}
输出结果如下:
数组长度=20字节,指针变量长度=8字节
a[0]=1
a[1]=2
a[2]=3
a[3]=100
a[4]=5
注意:指针变量如果指向的不是数组名或者数组的首元素的内存地址,那么用指针去取数组的元素的时候是有区别的,假设指针指向的是数组下标为2的元素的内存地址, 那么p[0] 就不再是等于a[0]了, 而是等于a[2]了, 同理p[1]就应该等于a[3],其他依次类推。
1.8 指针位移运算
指针运算不是简单的整数加减法,而是指针指向的数据类型在内存中占用字节数做为倍数的运算。 例如:char *p; 当执行p++; 后,移动的字节数是sizeof(char) 这么多的字节数;如果是int *p1; 那么p1++ 后移动的字节数就是sizeof(int) 。
#include<stdio.h>
int main()
{
int a[5] = {0};
int *p = a;
*p = 100;
printf("移动前指针位置:%p\n",p);
p += 2;
*p = 20;
printf("移动后指针位置:%p\n",p);
int i;
for(i = 0 ;i < 5; i++)
{
printf("a[%d]=%d\n",i,a[i]);
}
return 0;
}
结果如下:
移动前指针位置:0x16efbb600
移动后指针位置:0x16efbb608
a[0]=100
a[1]=0
a[2]=20
a[3]=0
a[4]=0
可以看到指针+2后,移动前后的内存地址刚好相差8个Byte,也就是2个int元素的占用的内存大小.
1.9 指针与char数组
在C语言中所有的数据类型都可以当做是一个char的数组.
#include<stdio.h>
int main()
{
int a = 0x12345678;
char *p = (char *)&a;
int i = 0 ;
for(i =0;i < sizeof(a); i++)
{
printf("a[%d]=%x\n",i,p[i]);
}
return 0;
}
练习-将一个int数转成ip地址 我们知道一个int占4个字节,刚好对应4个char, 对于unsigned char的取值范围就是0~255, 所以刚好和ip地址每段的取值符合.所以可以将int数转成ip地址。
#include<stdio.h>
int main()
{
int a = 987654321;
unsigned char *p = (unsigned char *)&a;
printf("%u.%u.%u.%u\n",p[3],p[2],p[1],p[0]);
return 0;
}
直接ping 987654321 对应的ip地址就是上面用程序求出的ip: 58.222.104.177
练习-将一个字符串IP转成unsigned int类型. 这个案例就是上面案例的反过程, 先将字符串中每一段的数据取出来,然后操作指针来赋值.
#include<stdio.h>
int main()
{
char a[] = "192.168.1.1";
unsigned int ip = 0;
unsigned char * p = (unsigned char *) &ip;
int a1,a2,a3,a4;
sscanf(a,"%d.%d.%d.%d",&a1,&a2,&a3,&a4);
p[0] = a4;
p[1] = a3;
p[2] = a2;
p[3] = a1;
printf("ip=%u\n",ip);
return 0;
}
如下所示,对数字3232235777进行ping时,查看的ip刚好就是192.168.1.1
练习-使用指针对二维数组进行排序
#include<stdio.h>
int main()
{
int a[2][3] = {{10,21,13},{42,5,9}};
int *p = (int *)a;
int i,j;
int size = sizeof(a) / sizeof(a[0][0]);
for(i = 0 ; i < size; i++)
{
for(j=0 ; j< size - i -1 ; j++)
{
if(p[j] > p[j+1])
{
int temp = p[j];
p[j] = p[j+1];
p[j+1] = temp;
}
}
}
for( i =0 ; i <sizeof(a) /sizeof(a[0]) ; i++)
{
for( j =0; j<sizeof(a[0]) / sizeof(a[0][0]); j++)
{
printf("%d,",a[i][j]);
}
}
return 0;
}
练习-使用指针对字符串取反操作
int main() {
char str[20] = "hello world";
char *start = str;
char *end = start + strlen(str) - 1;
while (start < end) {
char tmp = *start;
*start = *end;
*end = tmp;
start++;
end--;
}
printf("%s\n", str);
return 0;
}
1.10 指针数组
由于指针是一个变量,所以也可以以数组的形式出现。 指针数组的字节长度不受数据类型的影响,而是受到操作系统的影响, 在32位系统中,一个指针的长度是4个BYTE, 64位系统是8个BYTE, 所以32位系统中指针数组的长度等于 元素个数*4 ; 64位系统中指针数组的长度等于元素个数*8 .
#include<stdio.h>
int main()
{
int *a[10] = {NULL};
char *b[10] = {NULL};
printf("%lu,%lu\n",sizeof(a),sizeof(b));
return 0;
}
指针数组元素的赋值
#include<stdio.h>
int main()
{
int *a[10] = {NULL};
int i = 5;
a[0] = &i;
*a[0] = 100;
printf("i=%d\n",i);
return 0;
}
二、二级和多级指针
指针就是一个变量,既然是变量就也就存在内存地址,所以可以定义一个指向指针的指针变量。二级指针的定义会比一级指针多一个* ; 二级指针指向的是一级指针的地址, 一级指针指向的是变量的地址, 如下图所示: 换成代码就是:
int a = 10;
int *p = &a;
int **pp = &p;
*pp = NULL;
**pp = 100;
2.1 二级指针与一级指针数组的关系
一级指针数组里面存放的元素都是一级指针变量, 而指针数组的名字就是数组首元素的内存地址, 也就是一级指针的内存地址, 所以可以用二级指针的来指向.当指针指向数组的时候就可以当做数组本身来使用了.
int main()
{
int *a[3] = {NULL};
int x,y,z;
a[0] = &x;
a[1] = &y;
a[2] = &z;
int **p = a;
int i;
for(i=0;i<3;i++)
{
printf("p[%d]=%p\n",i,p[i]);
}
*p[0] = 100;
*p[1] = 200;
*a[2] = 300;
printf("x=%d,y=%d,z=%d\n",x,y,z);
return 0;
}
结果如下:
p[0]=0x16b1735f8
p[1]=0x16b1735f4
p[2]=0x16b1735f0
x=100,y=200,z=300
2.2 多级指针
每多加一级指针, 指针类型的* 号就会多一个.
#include<stdio.h>
int main()
{
int a = 10;
int *p = &a;
int **pp = &p;
int ***ppp = &pp;
int ****pppp = &ppp;
int *****ppppp = &pppp;
*****ppppp = 100;
printf("a=%d\n",a);
return 0;
}
三、指针在函数中的应用
3.1 在函数参数中使用指针
在C语言中规定,如果想通过函数内部修改外部实参的值,那么就需要给函数的参数传递实参的地址,否则是无法修改的。因为函数内对形参的改变不会影响到外部实参的值。
#include<stdio.h>
void swap1(int a, int b )
{
int temp = a;
a = b;
b = temp;
}
void swap2(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main()
{
int a = 1;
int b = 2;
swap1(a,b);
printf("swap1->a=%d,b=%d\n",a,b);
swap2(&a,&b);
printf("swap2->a=%d,b=%d\n",a,b);
return 0;
}
结果如下:
swap1->a=1,b=2
swap2->a=2,b=1
可以看到swap1并没有交换成功, 而swap2成功了.
3.2 数组名作为函数的参数
C语言规定当一个数组名作为函数的形参的时候,它就变成了指针变量名了,而不再是数组名了. 所以下面3种参数的写法其实是一样的
void test1(int a[10]);
void test2(int a[]);
void test3(int *a);
void test3(const int *a);
注意:如果把一个数组名作为函数的参数,那么在函数内部就不知道这个数组的元素个数了,因为sizeof算出来的是指针的大小, 32位系统是4个BYTE, 64位是8个BYTE, 所以如果想知道数组的长度,就需要增加一个参数来标明这个数组的大小. 除非数组是一个字符串, 那么可以不用传递长度.因为字符串可以有strlen方法来获取长度
#include<stdio.h>
void test(int *a)
{
printf("test->sizeof=%lu\n",sizeof(a));
a[2] = 100;
}
int main()
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
test(a);
printf("main->sizeof=%lu\n",sizeof(a));
int i;
for(i=0;i<sizeof(a)/sizeof(a[0]);i++)
{
printf("a[%d]=%d,",i,a[i]);
}
return 0;
}
3.3 函数指针
所谓函数指针就是指向函数内存地址的指针。一个函数在编译的时候会分配一个入口地址,这个入口地址就是函数的指针,函数名就是函数的入口地址。
函数指针的定义方式: 函数的返回值类型(*指针变量名称)(参数列表) 例如: int(*p)(int) 定义一个指向int func(int n) 类型的函数地址的指针.
函数可以通过函数指针来调用. int(*p)()代表一个指向函数的指针,但是没有固定指向哪一个函数, 这个可以动态指针.
void man() {
printf("抽烟\n");
printf("喝酒\n");
printf("打牌\n");
}
void woman() {
printf("化妆\n");
printf("逛街\n");
printf("网购\n");
}
int main() {
void (*p)();
int i = 0;
scanf("%d", &i);
if (i == 0)
p = man;
else
p = woman;
p();
return 0;
}
3.4 回调函数
将函数的指针作为另一个函数的参数时,这个参数就被称为回调函数。
#include <stdio.h>
int max(int a, int b) {
if (a > b)
return a;
else
return b;
}
int add(int a, int b) {
return a + b;
}
void func(int(*p)(int, int), int a, int b) {
int res = p(a, b);
printf("%d\n", res);
}
int main() {
int i = 0;
scanf("%d", &i);
if (i == 0)
func(max, 10, 20);
else
func(add, 10, 20);
return 0;
}
3.5 指针数组作为mian函数的形参使用
int main(int argc, char ** argv)
main函数是操作系统调用的,所以main函数的参数也是操作系统在调用的时候自动填写的,其中argc打包命令行参数的数量, argv代表命令行的具体参数.
下面写一个计算 2个数的和的程序,通过main函数的参数来完成.
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char **args)
{
if(argc <=2)
{
printf("参数不足,使用方式:%s 数字1 数字2\n",args[0]);
return 0;
}
int a = atoi(args[1]);
int b = atoi(args[2]);
printf("sum=%d\n",a+b);
return 0;
}
下面是一个四则运行实例代码
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char **args)
{
if(argc <=3)
{
printf("参数不足,使用方式:%s 数字1 运算符 数字2\n",args[0]);
return 0;
}
int a = atoi(args[1]);
char *c = args[2];
int b = atoi(args[3]);
switch(c[0])
{
case '+':
printf("%d%s%d=%d\n",a,c,b,a+b);
break;
case '-':
printf("%d%s%d=%d\n",a,c,b,a-b);
break;
case '*':
printf("%d%s%d=%d\n",a,c,b,a*b);
break;
case '/':
printf("%d%s%d=%d\n",a,c,b,a/b);
break;
}
return 0;
}
四、内存操作与指针
memset、memcpy、memmove这3个函数分别是内存的设置、内存的拷贝和内存的移动。使用时需要引入string.h头文件。
4.1 memset
表示设置内存,接收3个参数
void *memset(void *s, int c, size_t n);
参数1:是一个无类型的指针, 用于接收一个内存首地址,如果要操作数组,那就传数组名,因为数组名对应的是首元素的内存地址. 可以被指针变量接收. 参数2:要设置的值,如果填0就是0. 参数3:表示这块内存的大小,单位是字节 例如:
#include<stdio.h>
#include<string.h>
int main()
{
int a[5] = {1,2,3,4,5};
memset(a,0,sizeof(a));
int i;
for(i=0;i<5;i++)
{
printf("a[%d]=%d,",i,a[i]);
}
return 0;
}
4.2 memcpy
用于在2块内存之间拷贝数据,接收3个参数.
void *memcpy(void *dest, const void *src, size_t n);
参数1:目标地址的指针变量 参数2:源地址的指针变量, 且不能修改源地址变量的值,他是const修饰的. 参数3:拷贝多少内容,单位字节
#include<stdio.h>
#include<string.h>
int main()
{
int a[5] = {1,2,3,4,5};
int b[10] = {0};
memcpy(b,a,sizeof(a));
int i;
for(i=0;i<10;i++)
{
printf("b[%d]=%d,",i,b[i]);
}
return 0;
}
再来看看字符串的拷贝
#include<stdio.h>
#include<string.h>
int main()
{
char a[] = "hello";
char b[10] = {0};
memcpy(b,a,sizeof(a));
int i;
for(i=0;i<10;i++)
{
printf("b[%d]=%c\n",i,b[i]);
}
printf("b=(%s)\n",b);
return 0;
}
使用memcpy的时候,一定要确保内存没有重叠区域. 例如 :
int a[10] ={0};
memcpy(&a[3], &a[0], 20);
将a[0]之后的5个int数据拷贝到a[3]之后的5个int数据,他们中间下标3和4之间存在内存重叠区域.
4.3 memmove
表示内存移动, 参数也memcpy一样,用法也一样.
#include<stdio.h>
#include<string.h>
int main()
{
int a[10] = {0,1,2,3,4,5,6,7,8,9};
int b[10] = {0};
memmove(b,a,sizeof(a));
int i;
for(i=0;i<10;i++)
{
printf("a[%d]=%d,b[%d]=%d\n",i,a[i],i,b[i]);
}
return 0;
}
结果如下:
a[0]=0,b[0]=0
a[1]=1,b[1]=1
a[2]=2,b[2]=2
a[3]=3,b[3]=3
a[4]=4,b[4]=4
a[5]=5,b[5]=5
a[6]=6,b[6]=6
a[7]=7,b[7]=7
a[8]=8,b[8]=8
a[9]=9,b[9]=9
五、指针和字符串
在C语言中,大多数的字符串操作其实就是指针的操作.
5.1 通过指针修改字符串中的字符
#include<stdio.h>
int main()
{
char s[] = "hello world";
char *p = s;
printf("*p=%c\n",*p);
p[0] = 'a';
printf("*p=%c\n",*p);
return 0;
}
5.2 通过指针偏来修改字符串
#include<stdio.h>
int main()
{
char buf[100] = "hello world";
char *p = buf;
printf("*p=%c\n",*p);
*(p+3) = 'A';
printf("*p=%c,buf=%s\n",*p,buf);
p+=6;
printf("*p=%c\n",*p);
*p = 'B';
printf("buf=%s\n",buf);
return 0;
}
5.3 char *和字符串的关系
由于char * 是一级指针可以接收一个字符串变量(char 数组名) 或者字符串常量,所以按照%s输出指针变量其实是和输出数组名的效果一样的, 都是字符串内容. 如下所示:
char a[] = "hello";
char *c = a;
printf("c=%s,a=%s\n",c,a);
字符串常量和字符串变量定义和区别 1、字符串常量 定义:在一个双引号""内的字符序列或者转义字符序列称为字符串常量 例如:“HA HA!” “abc” “\n\t” 这些字符串常量是不能改变的,如果试图改变指针所指向的内容是错误的,因为字符串常量是存在静态内存区的,不可以改变的。 如定义字符串常量:
char* a="i love you.";
*a='h';
这是错误的。系统显示:
windows上报错如下:
string.exe 中的 0x00d71398 处未处理的异常: 0xC0000005: 写入位置 0x00d7573c 时发生访问冲突或者报该内存不能为written。
在gcc环境下报错如下:
Process finished with exit code 138 (interrupted by signal 10: SIGBUS)
2、字符串变量 在C语言中没有纯粹的c语言字符串变量,可以通过一个字符数组来体现,这样就可以对字符数组中的内容进行改变! 如定义字符串变量:
char a[]="i love you.";
*a='h';
5.4 char ** 和char*[]以及字符串的关系
char ** 是二级指针,用于接收一级指针的地址, 而char *[]是一级指针数组, 数组名就是首元素(一级指针)的地址,所以可以被char ** 接收, 那如果char ** 指向数组名,那么就可以当做数组使用. 所以就有下面的关系:
char a[] ="hello";
char *s[2];
s[0] = a;
char **p = s;
printf("p[0]=%p,s[0]=%p,a=%p,a[0]=%p\n",p[0],s[0],a,&a[0]);
printf("p[0]=%s,s[0]=%s,a=%s,a[0]=%c\n",p[0],s[0],a,a[0]);
printf("*p=%p,**p=%c\n",*p, **p);
结果如下:
p[0]=0x16b4bb5fc,s[0]=0x16b4bb5fc,a=0x16b4bb5fc,a[0]=0x16b4bb5fc
p[0]=hello,s[0]=hello,a=hello,a[0]=h
*p=0x16b4bb5fc,**p=h
总结: char** 指向char* 的数组, 所以char** 的指针变量可以当做char* 数组用, 而char* 指向char数组,所以char * 可以当做char数组用, 因此char ** 的指针变量可以直接操作char数组的元素.
|