一.内存操作
内存操作函数
1. 名称:malloc / calloc
功能:动态内存分配函数
头文件:#include <stdlib.h>
函数原型:void *malloc(size_t size);
void *calloc(int num, size_t size);
其中:num为分配内存块的个数,size为分配内存块的大小
返回值:分配成功返回分配内存块的首地址,失败返回NULL。
区别:(1)malloc一次只能申请一个内存区,calloc一次可以申请多个内存区;
(2)malloc不会对分配的内存初始化,calloc会初始化为0。
2. 名称:free
功能:动态内存释放函数
头文件:#include <stdlib.h>
函数原型:void free(void *ptr);
ptr为malloc或calloc等内存分配函数返回的内存指针。
返回值:无
注:malloc和calloc跟free是一一对应的关系,若动态分配内存没有free释放,容易造成内存泄露。
3. 名称:memcpy
功能:拷贝内存空间
头文件:#include <stdlib.h>
函数原型:void *memcpy(void *dest, void *src, size_t n);
其中:dest为目标内存区,src为源内存区,n为需要拷贝的字节数
返回值:指向dest的指针
局限性:未考虑内存重叠情况
函数实现:
void *memcpy(void *dest, void *src, size_t n)
{
char *ret = char *dest;
char *dest_t = ret;
char *src_t = char *src;
while(n--)
{
*dest_t++ = *src_t++;
}
return ret;
}
4. 名称:memmove
功能:拷贝(移动)内存空间
头文件:#include <stdlib.h>
函数原型:void *memmove(void *dest, void *src, size_t n);
其中:dest为目标内存区,src为源内存区,n为需要拷贝的字节数
返回值:指向dest的指针
相比memcpy:当dest与src重叠时,仍能正确处理,但是src内容会被改变
函数实现:
-
void *memmove(void *dest, void *src, size_t n) { char *ret = char *dest; char *dest_t; char *src_t; if((unsigned char *dest <= unsigned char *src) || (unsigned char *dest >= unsigned char *src + n)) { dest_t = char *dest; src_t = char *src; while(n--) { *dest_t++ = *src_t++; //正向拷贝 } } else { dest_t = char *dest + n - 1; src_t = char *src + n - 1; while(n--) { *dest_t-- = *src_t--; //反向拷贝 } } return ret; }
5. 名称:memset
功能:初始化指定内存空间
头文件:#include <stdlib.h>
函数原型:void *memset(void *buffer, int c, size_t n);
其中:buffer为分配的内存,c为初始化你内容,n为初始化的字节数
返回值:指向buffer的指针
特别注意:memset是按字节为单位对buffer指向的内存赋值
例:int a[5];
memset(a, 3, 5*sizeof(int)); //错误
上述情况下每个int元素被初始化为:00000011 00000011 00000011 00000011
memset(a, 0, 5*sizeof(int)); //正确,全部初始化为0
memset把buffer所指内存区域的前count个字节设置成某个字符的ASCLL值.一般用于给数组,字符串等类型赋值.
main() { int *p=NULL; int i; char *q=NULL;
p=(int *)malloc(sizeof(int)*10); if(p==NULL) exit(1); memset(p,0,sizeof(int)*10); q=p; for(i=0;i<10;i++) printf("%d",*(q++)); free(p); } |
执行结果是10个0.
6. 名称:memcmp
功能:比较两个内存空间的字符
头文件:#include <stdlib.h>
函数原型:int memcmp(const void *buf1, const void *buf2, size_t n);
其中:n为要比较的字符数
返回值:当buf1 > buf2时,返回 > 0;当buf1 = buf2时,返回 = 0;当buf1 < buf2时,返回 < 0。
二.指针
所谓指针,也就是内存的地址;所谓指针变量,也就是保存了内存地址的变量。
需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
1、指针变量的定义和使用
数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。
在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外一个普通变量或者指针变量。
假设有一个char类型的变量c,它存储了字符'K',并占用了地址为0x11A的内存(地址通常用十六进制表示)。
另外有一个指针变量p,它的值为0x11A,正好等于变量c的地址,这种情况我们就称p指向了c,或者说p是指向变量c的指针。
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*
,格式为:
-
datatype *name; 或者 datatype *name = value; 其中,*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型。
*
是一个特殊符号,表明一个变量是指针变量,定义指针变量时必须带*
。而给指针变量赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*
,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*
,给指针变量赋值时不能带*
。
而去指针变量也可以连续定义
定义指针变量时的 * 和使用指针变量时的 * 区别:
int *p = &a;//*用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;
*p = 100;//在指针变量前面加*表示获取指针指向的数据
需要注意的是,给指针变量本身赋值时不能加*,如:
int *p;
p = &a;//如果写成*p = &a,报错!!!
*p = 100
//通过指针交换两个变量的值
#include <stdio.h>
int main(){
int a = 100, b = 999, temp;
int *pa = &a, *pb = &b;
printf("a=%d,b=%d\n", a, b);
//开始交换
temp = *pa;
*pa = *pb;
*pb = temp;
//结束交换
printf("a=%d,b=%d\n", a, b);
return 0;
}
//运行结果:
a=100,b=999
a=999,b=100
最后, 星号*
主要有三种用途:
①表示乘法,例如int a = 3, b = 5, c; c = a * b;
,这是最容易理解的。
②表示定义一个指针变量,以和普通变量区分开,例如int a = 100; int *p = &a;
。
③表示获取指针指向的数据,是一种间接操作,例如int a, b, *p = &a; *p = 100; b = *p;
二、指针变量运算(加法、减法、比较运算)
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算,例如加法、减法、比较等。但是不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。
三、数组指针(指向数组的指针)
数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };
为例,该数组在内存中的分布如下图所示:
定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:
如果一个指针指向了数组,我们就称它为数组指针(Array Pointer),定义一个指向数组的指针例子如下:
-
int arr[] = {23, 56, 4, 34, 45 }; int *p = arr; arr 本身就是一个指针,可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头。
数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *
引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。
(1) 使用下标
也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。
(2) 使用指针
也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)。
不管是数组名还是数组指针,都可以使用上面的两种方式来访问数组元素。不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。
最后
(1) 指针变量可以进行加减运算,例如p++
、p+i
、p-=i
。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。
(2) 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;
是没有意义的,使用过程中一般会导致程序崩溃。
(3) 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL
。
(4) 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。
(5) 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。
三.结构体指针
一个结构体变量的指针就是该结构体变量所占据内存段的起始地址。
可以设一个指针变量,用来指向一个结构体变量,此时该指针变量的值是结构体变量的起始地址。
同时指针变量也可以用来指向结构体数组中的元素。
结构体指针变量说明的一般形式为:
struct 结构名 *结构指针变量名
例如,在前面的例题中定义了stu这个结构,如果要说明一个指向stu的指针变量pstu,可写为
struct stu *pstu;
当然也可以在定义stu结构时同时说明pstu。与前面讨论的各类指针变量相同,结构体指针变量也必须要先赋值才能使用
赋值就是把结构变量首地址赋予该指针变量,不能把结构名赋予该指针变量。
如果boy是被说明为stu类型的结构变量,则:
pstu = &boy;是正确的
pstu = &stu;是错误的
因为结构名和结构变量是两个不同的概念,不能混淆。结构名只能表示一个结构形式,编译系统并不对他们分配内存空间,只有当某变量被说明为这种类型的结构时,才对该变量存储内存空间。
所以以上 pstu = &stu;是错误的,不可能去取一个结构名的首地址。有了结构指针变量,就更方便地访问结构变量的各个成员。
其访问的一般形式为:
(*结构指针变量).成员名
或为:
结构指针变量->成员名
例如:(*pstu).num
或者:pstu->num
案例:
struct stu{
int num;
char *name;
char sex;
float score;
}boy1 = {006,"zhangzhang",'1',69.6};
int main() {
struct stu *pstu;
pstu = &boy1;
printf("%d,%s\n", boy1.num, boy1.name);
printf("%d,%s\n", (*pstu).num, (*pstu).name);
printf("%d,%s\n", pstu->num, pstu->name);
}
关于结构体指针的传递和调用的用法:
上述调用中采用的结构体变量。在传入函数时通过指针void *para指针传递过去。需要注意的是不能直接使用para->a来访问结构体的成员。这是因为para只是接收过来的地址。para虽然指向的结构体的首地址。但是这个指针并不知道自己指向的是什么内容和有多少成员。需要(date *)para强制转化一下。这样para就可以知道自己是什么类型的指针,有多少成员。