第一章 指针
1 指针概述
1.1 内存概述
指针:内存的编号
1.2 指针和指针变量
- 内存区的每一个字节都有一个编号,这就是“地址”。
- 如果在程序中定义了一个变量,在对程序进行编译或运行时,系统就会给这个变量分配内存单元,并确定它的内存地址(编号)
- 指针的实质就是内存“地址”。指针就是地址,地址就是指针。
- 指针是内存单元的编号,指针变量是存放地址的变量。
- 通常我们叙述时会把指针变量简称为指针,实际他们含义并不一样。
32位系统中:0x0000 0000,一个地址用32位表示,即4字节,指针变量占4字节
64位系统中:内存的编号范围是0x0000000000000000 - oxffffffffffffffff ,这样的编号,需要8个字节才能存下,所以指针变量也需要8个字节
2 指针基础知识
2.1 指针变量的定义和初始化
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
- “*”操作符操作的是指针变量指向的内存空间
定义指针的三步骤:
int a = 10;
1 *与符号结合代表是一个指针变量
2 要保存谁的地址,将他的定义形式放在此处
3 用*p替换掉定义的变量
int a ---> int (*p) ---> int *p
int *p = &a;
分析:
1 p是变量名,值是内存地址
p的类型为:将变量p隐藏剩下的,即在本例中为int *类型
2 指针变量p用来保存什么类型数据的地址:
将指针变量p和离p最近的*一起隐藏
在本例中: int *p ----> int :即该指针变量保存int类型数据的地址
使用时:*与p结合代表,取p指针所指向那块空间的内容
*p = 100;
注意:&可以取得一个变量在内存中的地址。但是,不能取寄存器变量,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。
2.1.1 关于指针匹配问题
有时候会保存多级的地址,int*p, int **q, *q = &p等,很容易搞错。
下面介绍一个原则:
=在使用时,对一个表达式取 *,就会对表达式减一级 *,如果对表达式取&,就会加一级 *=
2.2 指针大小
- 使用sizeof()测量指针的大小,得到的总是:4或8
- sizeof()测的是指针变量指向存储地址的大小
- 在32位平台,所有的指针(地址)都是32位(4字节)
- 在64位平台,所有的指针(地址)都是64位(8字节)
int *p1;
int **p2;
char *p3;
char **p4;
printf("sizeof(p1) = %d\n", sizeof(p1));
printf("sizeof(p2) = %d\n", sizeof(p2));
printf("sizeof(p3) = %d\n", sizeof(p3));
printf("sizeof(p4) = %d\n", sizeof(p4));
printf("sizeof(double *) = %d\n", sizeof(double *));
指针变量只用来保存地址,不管有多少级,指针地址都是4字节或8字节
不同类型的指针变量, 取指针指向的内容的宽度
指针的宽度 = sizeof(将指针变量与指针变量最近的*隐藏,剩下的类型)
宽度也叫做步长;
步长: 指针加1跨过多少个字节
char *p 1
short *p 2
int *p 4
Int **p 4 sizeof(int *)
int main()
{
int num = 0x01020304;
char *p1 = (char *)#//int *
short *p2 = (short *)#
int *p3 = #
//通过*取指针变量所指向那块空间内容时,取的内存的宽度和指针变量本身的类型有关
printf("%x\n",*p1);
printf("%x\n", *p2);
printf("%x\n", *p3);
system("pause");
return 0;
}
2.3 野指针与空指针
野指针就是没有初始化的指针,指针的指向是随机的,不可以 操作野指针。
此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
//野指针
int main()
{
int *p;//野指针
*p = 200;
printf("%d\n",*p);
system("pause");
return 0;
}
空指针:把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。
int main()
{
int a;
//将指针的值赋值为0,0x0000000 = NULL
//赋值为null只是一个标志,代表此指针还没被使用
int *p = NULL;//给指针p的内容赋值为0
*p = 200;//err 因为p保存了0x0000的地址,这个地址是不可以使用的,非法,
printf("%d\n",*p);
system("pause");
return 0;
}
空指针的作用: 如果使用完指针将指针赋值为NULL,在使用时判断一下指针是否为NULL,就知道指针有没有被使用
2.4 万能指针void *
可以保存任意变量的地址
说明:
void * 可以保存所有类型的地址,
但是用该指针(通过地址)去取值的时候,由于void不知道取多少字节,所以无法取值。
解决:
需要强转给指定的步长(sizeof)去寻址取值。
//万能指针
int main()
{
//void b; 不可以定义void类型的变量,因为编译器不知道给变量分配多大的空间
//但是可以定义void *类型,因为指针都是4个字节
int a = 10;
short b = 10;
void *p = (void *)&a;//万能指针可以保存任意的地址,为了保持前后统一,进行强转
void *q = (void *)&b;
//printf("%d\n", *p);//err p是void*,不知道取几个字节的大小,无法根据地址取值
printf("%d\n",* (int *)p);// 先把地址转为int *类型,再取值,即*( (int *)地址)
system("pause");
return 0;
}
2.5 const修饰的指针变量
const修饰的普通数据类型变量,不能通过变量名修改,可以通过它的地址去修改
const int c = 10;
int *p = &c;
*p = 100;
const修饰指针变量:
1 指向内容不能改变
2 指向不能改变
3 都不能改变
int main()
{
int a = 10;
int b = 20;
//const修饰的是 * 还是变量p,
//const修饰的是*
//const int *p = &a; //不能通过 *p,改p所指向空间的内容
//*p = 100; // err 指向空间内容不能改变
//const修饰的变量p
//p保存的地址不可以修改
//int * const p = &a;
//p = &b;err p本身的值不能被更改
const int *const p = &a; //不能通过*p修改指向空间内容,且不能改变p指向
system("pause");
return 0;
}
2.6 多级指针
定义多级指针保存数据的地址时,定义的指针的类型只需要比要保持的数据的类型多一级*
//多级指针
int main()
{
int a = 10;
//*p int a int *p
int *p = &a;
//*q int *p int **q
int **q = &p;
//如果*和&相遇,相抵消
// **q == *(*q) == *(p) == a
//**q == *(*q) == *(&a) == a
printf("%d\n", **q);
// *k int **q int ***k
int ***k = &q;
//*符号结合,代表这个k是一个指针变量
//k是一个变量
//k的类型,将变量k隐藏,剩下的类型
//k用来保存谁的地址 将变量k和k最近的*一起隐藏,剩下什么类型
//就保存什么类型数据的地址
int *******************g;
int ********************f = &g;
system("pause");
return 0;
}
3 指针与数组
3.1 数组名
数组名字是数组的首元素地址,但它是一个常量
int a[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
printf("a = %p\n", a);
printf("&a[0] = %p\n", &a[0]);
//a = 10; //err, 数组名只是常量,不能修改
3.2 指针操作数组元素
数组名a即数组的地址不可以改变,但是使用指针就可以指向数组某个元素
int main()
{
//int a[10] = {1,2,3,4,5,6,7,8,9,10};
int a[10] = { 0 };
//a 数组名,首元素的地址
int *p = a;//指针p保存的是首元素的地址
for (int i=0;i<sizeof(a)/sizeof(a[0]);i++)
{
//printf("%d ",a[i]);
//printf("%d ", *(p+i));
*(p + i) = i;
}
for (int i = 0; i<sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ",a[i]);
//printf("%d ", *(p+i));
//*(p + i) = i;
}
system("pause");
return 0;
}
指针加1,跨过一个步长
int *p;
步长 = sizeof(int)
步长 = sizeof(将p和最近的*隐藏即为步长)
要得到内存的数据,就该先得到数据的地址
*(地址) 得到的是地址里面的内容
3.3 指针加减运算
加法:两指针相加没有意义,只限跨步长的加法
- 如果是一个int *,+1的结果是增加一个int的大小
- 如果是一个char *,+1的结果是增加一个char大小
两指针(类型一致)相减,得到的是中间跨过多少元素
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};
//sizeof(int [10])
int *p = a;
//int *q = (int *)(&a + 1) - 1;
int *q = &a[9];
printf("%d\n",q-p);// p+9 == q
printf("%d\n",*(p+3));
//两指针相加没有意义
// printf("%d\n", p+q);err
system("pause");
return 0;
}
tips:
int *q = (int *)(&a + 1) - 1;
&a + 1代表跨过整个数组
要想指针指向数组末尾元素,必须向前跨一个元素,但是直接减一是向前跨整个元素
所以需要根据数组的元素类型,先转为int*指定步长后,再向前减一移动一个元素的长度
3.4 [] == *()
p[i] == *(p+i)
int main()
{
//[] == *()
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
int *p = a;
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
//printf("%d ",a[i]);//a[i] == *(a+i)
//printf("%d ", *(p+i));
//printf("%d ", p[i]);// p[i] == *(p+i)
printf("%d ", *(a + i));
}
system("pause");
return 0;
}
3.5 指针数组
指针数组,它是数组,数组的每个元素都是指针类型。
int main()
{
int a = 10;
int b = 20;
int c = 30;
// int *p1 = &a int *p2 = &a int *p2 = &a
//需求: 数组中的每一个元素都是指针(地址)
int *num[3] = {&a,&b,&c};
printf("%d\n",sizeof(num));
//&a == num[0]
for(int i=0;i<sizeof(num)/sizeof(num[0]);i++)
{
//先通过下标取到地址 num[i],再*取值 *num[i]
printf("%d\n",*num[i]);
}
system("pause");
return 0;
}
定义一个指针用来保存数组num首元素的地址:
1 num[0]是int* 类型
2 用指针保存则定义int *k
3 将int *k替换 num[0] 则:int **类型
int main()
{
int a = 10;
int b = 20;
int c = 30;
int *num[3] = {&a,&b,&c};
//定义一个指针用来保存数组num首元素的地址
// num == &num[0] = &(int *) == int **
//num[0]是int *类型,要保持int *类型的地址,需要比它多一级*
int **k = num;
for (int i = 0; i < sizeof(num) / sizeof(num[0]); i++)
{
printf("%d ",**(k+i));
}
system("pause");
return 0;
}
第一级*:取值取到的仍然是地址
第二级*:取到值
4 指针和函数
4.1 指针作为函数的形参
值传递,只改变传递后的形参的值,作用的是形参,并未与实参建立任何联系。
指针作为函数的形参,可以改变实参的值,形参拿到是实参的地址,根据地址直接取实参内存空间的里的值操作
,
4.2 数组名做函数参数
数组作为函数的形参会退化为指针
如:int b[10] === int *b
另外只传数组名相当于只初始化了一个指针,并不能知道数组的长度,需要将长度传给函数
//void print_arr(int b[10]) // int *b
//void print_arr(int b[1000])//int *b
void print_arr(int *b,int len)
{
int n = sizeof(b) / sizeof(b[0]); // *(b+0) == *b
printf("n=%d\n",n);
for (int i = 0; i <len; i++)
{
printf("%d ",b[i]);
}
printf("\n");
}
int main()
{
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
print_arr(a,sizeof(a)/sizeof(a[0]));//打印数值的内容// &a[0] int *
system("pause");
return 0;
}
4.3 指针作为函数的返回值
int num = 0;//在函数外面定义的变量叫全局变量,整个工程都可以使用
//整个变量程序启动开辟空间,直到程序结束释放空间
int * getnum()
{
//{}中定义的变量叫局部变量,局部变量在函数结束之后的空间会被释放
srand(time(NULL));
num = rand();
return #//
}
int main()
{
int * p = getnum();
printf("%d\n",*p);
system("pause");
}
以上如果 不把num定义为全局变量,那么getnum函数执行完就销毁了,这时候拿到num的地址是没有意义的,因为num内存空间已经被销毁了。
5 指针和字符串
5.1 字符指针
int main()
{
//指针与字符串
char a[] = "helloworld";//定义了一个字符数组,字符数组内容为helloworld\0
//定义一个指针用来保存数组首元素的地址
char * p = a;
printf("%s\n",p);//%s打印一个字符串,要的是首个字符的地址
printf("%s\n", p + 2);
printf("%c\n",*(p+3));
*p = 'm';
printf("%s\n", p);
//a++
p++;
*p = 'o';
printf("%s\n", p);
//printf("%d\n",sizeof(a));//11
//printf("%d\n", sizeof(p));//4
//printf("%d\n", strlen(a));//
//printf("%d\n", strlen(p));//
system("pause");
return 0;
}
5.2 字符串常量
“hello” 字符串常量是不可以改变的,存在文字常量区
在使用时.双引号""代表取这个字符串首元素的地址
char *p = “hello”; //代表将字符串常量的地址赋值给指针p
int main()
{
char a[] = "helloworld";//定义了一个字符数组,字符数组内容为helloworld
char * p = a;//定义一个指针用来保存数组首元素的地址
p = "abcdef";//字符串常量存文字常量区,""在使用时,取的是字符串首元素的地址
//文字常量区的内容是不可以改变的
printf("%s\n",p);
printf("%d\n",sizeof(p));//4
printf("%d\n", sizeof("abcdef"));//7
printf("%d\n", strlen(p));//6
printf("%d\n", strlen("abcdef"));//6
*p = 'm';//err p指向的是文字常量区,文字常量区的内容是不能改变的
printf("%s\n",p);
system("pause");
return 0;
}
5.3 字符指针作为形参
//字符串已经有了\0的标志,不需要传长度,只需要两个字符串作为参数
char * my_strcat(char * src, char *dst)
{
int n = strlen(src);
int i = 0;
while (*(dst + i) != 0)
{
*(src+n+i) = *(dst + i);
//src[n+i] = dst[i] ;
i++;
}
*(src + n + i) = 0;
return src;
}
int main()
{
char str1[128] = "hello";//hello123456\0
char str2[128] = "123456";
my_strcat(str1,str2);
printf("%s\n",my_strcat(str1, str2));
system("pause");
return 0;
}
tips:字符串已经有了\0的标志,不需要传长度
5.4 const修饰的指针变量
int main()
{
char buf[] = "hello";
char str[] = "acbg";
const char *p = buf;//const修饰指针,不能通过指针修改指针所指向的空间内容
//*p = 'b'; err 不能通过p指针修改那块空间的内容
char * const k = buf;//指针变量k初始化之后不能改变k指针变量本身的指向
// k = "world"; err
// k = str; err
system("pause");
return 0;
}
5.5 字符指针数组
//字符指针数组
//是一个数组,每一个元素是字符指针
int main()
{
/*char *p1 = "heihei";
char *p2 = "haha";
char *p3 = "xixi";*/
//char *num[3] = { p1,p2,p3};
char *num[3]={ "heihei" ,"haha","xixi"};
//定义一个指针保存num数组首元素的地址 &num[0] == num
char **p = num;
for (int i = 0; i < 3; i++)
{
// printf("%s\n",*(p+i));
printf("%s\n", p[i]);//打印字符数组,需要首元素的起始地址
}
printf("%c\n",*(*(p+1)+3));// *(p[1]+3) == p[1][3]
//for (int i = 0; i < 3; i++)
//{
// printf("%s\n",num[i]);
//}
//printf("%c\n",*num[0]);//
//printf("%c\n", *(num[1] + 1));
//printf("%c\n", *(num[2]+2));
system("pause");
return 0;
}
应用1:字符指针数组作为main函数的形参
int main(int argc, char *argv[]);
- main函数是操作系统调用的,第一个参数标明argc数组的成员数量,argv数组的每个成员都是char *类型
- argv是命令行参数的字符串数组
- argc代表命令行参数的数量,程序名字本身算一个参数
//.*.exe hello 123456
//char *argv[] = { ".*.exe", "hello" "123456" };
int main(int argc,char *argv[])
{
/*printf("%d\n",argc);
printf("%s\n", argv[0]);
printf("%s\n", argv[1]);
printf("%s\n", argv[2]);
printf("%s\n", argv[3]);*/
for (int i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);//字符数组内元素的首地址
}
system("pause");
return 0;
}
5.6 字符串处理函数
字符串函数(即:str开头的函数)的头文件都为#include <string.h>
strcpy()
strcpy(str1,str2);//将str2的字符拷贝至str1数组中,注意,str2遇到\0结束,会将\0拷贝至str1
int main()
{
char str1[128] = "heiheihaha";
char str2[128] = "world";
strcpy( str1 , str2);
printf("%s\n",str1);
system("pause");
return 0;
}
strncpy()
strncpy(str1,str2,n);//将str2中的前n个字符拷贝至str1中,如果拷贝时不足n个,遇到\0拷贝结束
int main()
{
char str1[1024] = "";//wo\0\0\0 wo\0rl
char str2[128] = "wo\0rldhello";
//strcpy( str1 , str2);
strncpy(str1,str2,5);
// printf("%s\n",str1);
for (int i = 0; i < 5; i++)
{
printf("%d ",str1[i]);
}
system("pause");
return 0;
}
strcat()
strncat()
strcat(str1,str2)//将str2字符数组中的字符连接到str1字符数组中,遇到\0结束
strncat(str1,str2,n)//将str2字符数组中的n个字符连接到str1字符数组中,遇到\0结束
int main()
{
char str1[1024] = "123456";//wo\0\0\0 wo\0rl
char str2[128] = "worldhello";
//strcat(str1,str2);//将str2的字符串拷贝到str1字符串的后面
strncat(str1, str2,5);
printf("%s\n",str1);
system("pause");
return 0;
}
strcmp()
strncmp()
注意: 比较时遇到\0结束比较
int main()
{
char str1[] = "a\0bcdef";
char str2[] = "a\0cdrrr";
//str1数组中和str2数组拿出一个元素进行比较,相等继续往后比较
//比较的是字符的ascii值
//如果str1> str2 返回值等于1
//如果str1== str2 返回值等于0
//如果str1 <str2 返回值等于 - 1
//printf("%d\n",strcmp(str1,str2));
printf("%d\n", strncmp(str1, str2,3));
system("pause");
return 0;
}
sprintf()
组包函数
int len = sprintf(buf,“格式”,“数据”);//将数据安装格式组包,存放在数组buf中
中间有\0的字符串组成的包,我想知道这个包的长度,而不是字符串的长度(strlen),怎么求?
sprintf有返回值,返回的是组完包的有效长度
int main()
{
int year = 2018;
int month = 10;
int day = 20;
char buf[1024] = "";
//printf("year=%d month=%d day=%d \n",year,month,day);
int len = sprintf(buf,"year=%d %cmonth=%d day=%d \n",year, 0,month, day);
//printf("%d\n",strlen(buf));
printf("len=%d\n",len);
printf("buf=[%s]",buf);
system("pause");
return 0;
}
sscanf()
拆包函数
sscanf(buf,“格式”,数据);//将buf的内容格式化输出到数据
int main()
{
//%d 0-9的字符
int year =0 ;
int month = 0;
int day = 0;
char buf[1024] = "beijing:2018:t:10:20";
//scanf("%d:%d:%d",&year,&month,&day);//从键盘按照相应的格式获取数据
//格式按照别人的格式,我要什么数据就按那个数据的格式提取
sscanf(buf, "beijing:%d:t:%d:%d", &year, &month, &day);//从buf中按照`相应的格式获取数据
printf("%d %d %d\n",year,month,day);
system("pause");
return 0;
}
strchr()
strchr(buf,ch)//在buf字符数组中查找字符ch出现的位置,如果成功返回此字符出现位置的地址,如果没有找到,返回NULL
char *my_strchr(char *p, char ch)
{
int i = 0;
while (p[i] != 0)
{
if (p[i] == ch)
return &p[i];
i++;
}
if (p[i] == 0)
return NULL;
}
int main()
{
char str[] = "xixihellogworld";
char *p = strchr(str,'g');
//char *p =my_strchr(str,'g');
printf("%s\n",p);
system("pause");
return 0;
}
strstr()
strstr(str1,str2)//在str1字符数组中查找str2字符串出现的位置,并且返回这个位置的地址
char *my_strstr(char *str1, char *str2)
{
int i = 0;
while (str1[i] != 0)
{
if (str1[i] == str2[0])
{
if (0 == strncmp(str1 + i, str2, strlen(str2)))
return str1 + i;
}
i++;
}
if (str1[i] == 0)
return NULL;
}
int main()
{
char str1[] = "helloaabcfhaffjhafafha";
char str2[] = "abc";
//在str1中查找str2字符出现的位置
//先找a字符,如果找到了a字符,在比较
char *p = strstr(str1,str2);
printf("%s\n",p);
system("pause");
return 0;
}
strtok()
来将字符串分割成一个个片段。当strtok()在参数s的字符串中发现参数delim中包含的分割字符时, 则会将该字符改为\0 字符,当连续出现多个时只替换第一个为\0。
int main()
{
char str[] = "15080015225#bangquan#82263#123456";
//第一次切
//char *p1 = strtok(str,"#");//在str1中将#切割,返回切割前面的字符串
//printf("%s\n",p1);
//第二次注意不能再写str,不然还是返回上一次前面切割的一个字符串
//第二次切割要从 \0 ——>NULL 后面开始切
//char *p2 = strtok(NULL,"#");
//printf("%s\n",p2);
//第三次切也是从NULL,会自动记录切的位置,往后切
//char *p3 = strtok(NULL, "#");
//printf("%s\n", p3);
char *p[10] = {NULL};//初始化指针数组元素全部为NULL
int i = 0;
do {
//第一次切和后面的切不同
if(i == 0)
p[i] = strtok(str, "#");
//后面切
else
p[i] = strtok(NULL, "#");
} while ( p[i++] != NULL);//如果strtok的返回值等于NULL,代表切割完毕
//输出指针字符数组
int j = 0;
while (p[j] != NULL)
{
printf("%s\n",p[j++]);
}
system("pause");
return 0;
}
tips:
如果遇到字符串为char str[] = "15080015225$bangquan#82263$123456";
要求:遇到$或者#都要切割
那么写为strtok(NULL, "#$");
"#$"代表遇到#或者$就切割
总结:
- 在第一次调用时:strtok()必需给予参数s字符串
- 往后的调用则将参数s设置成NULL,每次调用成功则返回指向被分割出片段的指针
char a[100] = "adc*fvcv*ebcy*hghbdfg*casdert";
char *s = strtok(a, "*");//将"*"分割的子串取出
while (s != NULL)
{
printf("%s\n", s);
s = strtok(NULL, "*");
}
atoi()
atoi()会扫描字符串,跳过前面的空格字符,直到遇到数字或正负号才开始做转换,而遇到非数字或字符串结束符(’\0’)才结束转换,并将结果返回返回值。
atoi : 将字符串转整数
atof: 将字符串转float类型的数据
atol: 将一个字符串转化为long类型
char str1[] = "-10";
int num1 = atoi(str1);
printf("num1 = %d\n", num1); //-10
char str2[] = "0.123";
double num2 = atof(str2);
printf("num2 = %lf\n", num2);