C语言复习笔记 12

来源:互联网 发布:查看网络延时命令 编辑:程序博客网 时间:2024/06/04 20:12

练习代码能理解————OK

#include <stdio.h>int main(){    char str[20] = "c.biancheng.net";    char *s1 = str;    char *s2 = str+2;    char c1 = str[4];    char c2 = *str;    char c3 = *(str+4);    char c4 = *str+2;    char c5 = (str+1)[5];    int num1 = *str+2;    long num2 = (long)str;    long num3 = (long)(str+2);    printf("  s1 = %s\n", s1);    printf("  s2 = %s\n", s2);    printf("  c1 = %c\n", c1);    printf("  c2 = %c\n", c2);    printf("  c3 = %c\n", c3);    printf("  c4 = %c\n", c4);    printf("  c5 = %c\n", c5);    printf("num1 = %d\n", num1);    printf("num2 = %ld\n", num2);    printf("num3 = %ld\n", num3);    return 0;}
 s1 = c.biancheng.net  s2 = biancheng.net  c1 = a  c2 = c  c3 = a  c4 = e  c5 = cnum1 = 101num2 = 2686736num3 = 2686738

C语言指针变量作为函数的参数

在C语言中,函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。

有的时候,对于整数、小数、字符等基本类型数据的操作也必须要借助指针,一个典型的例子就是交换两个变量的值。

总结
1、 指针作参数可以将一个变量的地址进行传递,从而可以在函数内部改变变量的值,不会随函数的调用结束而恢复。——与内存相关

2、指针可以传递批量数据。想数组和字符串,我们可以通过传递数组或字符串的首地址,然后在函数中就可以批量进行操作数据了。——地址

用数组作函数参数

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。

用数组作函数参数

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。下面的例子定义了一个函数 max(),用来查找数组中值最大的元素:

#include <stdio.h>int max(int *intArr, int len){    int i, maxValue = intArr[0];  //假设第0个元素是最大值    for(i=1; i<len; i++){        if(maxValue < intArr[i]){            maxValue = intArr[i];        }    }    return maxValue;}int main(){    int nums[6], i;    int len = sizeof(nums)/sizeof(int);    //读取用户输入的数据并赋值给数组元素    for(i=0; i<len; i++){        scanf("%d", nums+i);    }    printf("Max value is %d!\n", max(nums, len));    return 0;}运行结果:12 55 30 8 93 27↙Max value is 93!

参数 intArr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数,scanf() 在读取用户输入的整数时,要求给出存储它的内存的地址,nums+i就是第 i 个数组元素的地址。

用数组做函数参数时,参数也能够以“真正”的数组形式给出。例如对于上面的 max() 函数,它的参数可以写成下面的形式:

int max(int intArr[6], int len){    int i, maxValue = intArr[0];  //假设第0个元素是最大值    for(i=1; i<len; i++){        if(maxValue < intArr[i]){            maxValue = intArr[i];        }    }    return maxValue;}

int intArr[6]好像定义了一个拥有 6 个元素的数组,调用 max() 时可以将数组的所有元素“一股脑”传递进来。

读者也可以省略数组长度,把形参简写为下面的形式:

int max(int intArr[], int len){    int i, maxValue = intArr[0];  //假设第0个元素是最大值    for(i=1; i<len; i++){        if(maxValue < intArr[i]){            maxValue = intArr[i];        }    }    return maxValue;}

int intArr[]虽然定义了一个数组,但没有指定数组长度,好像可以接受任意长度的数组。

实际上这两种形式的数组定义都是假象,不管是int intArr[6]还是int intArr[]都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为int *intArr这样的指针。这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针

int intArr[6]这种形式只能说明函数期望用户传递的数组有 6 个元素,并不意味着数组只能有 6 个元素,真正传递的数组可以有少于或多于 6 个的元素。

需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。

对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。

除了C语言,C++、Java、Python 等其它语言也禁止对大块内存进行拷贝,在底层都使用类似指针的方式来实现。

用C语言指针作为函数返回值

关于gets函数的使用

    char str1[5];  //不要char*p,然后gets(p),这是错误的,因为p没有指向有效的内存,它可能指向任何非法地址                   //     地方的未知大小的内存块,这样以来,就可能修改了不属于本程序的内存的内容    gets(str1); 

用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。请看下面的例子:

#include <stdio.h>int *func(){    int n = 100;    return &n;}int main(){    int *p = func(), n;    n = *p;    printf("value = %d\n", n);    return 0;}
运行结果:value = 100

n 是 func() 内部的局部变量,func() 返回了指向 n 的指针,根据上面的观点,func() 运行结束后 n 将被销毁,使用 *p 应该获取不到 n 的值。但是从运行结果来看,我们的推理好像是错误的,func() 运行结束后 *p 依然可以获取局部变量 n 的值,这个上面的观点不是相悖吗?

#include <stdio.h>int *func(){    int n = 100;    return &n;}int main(){    int *p = func(), n;    printf("c.biancheng.net\n");    n = *p;    printf("value = %d\n", n);    return 0;}
运行结果:c.biancheng.netvalue = -2

可以看到,现在 p 指向的数据已经不是原来 n 的值了,它变成了一个毫无意义的甚至有些怪异的值。与前面的代码相比,该段代码仅仅是在 *p 之前增加了一个函数调用,这一细节的不同却导致运行结果有天壤之别,究竟是为什么呢?

前面我们说函数运行结束后会销毁所有的局部数据,这个观点并没错,大部分C语言教材也都强调了这一点。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。

C语言二级指针(指向指针的指针)

指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int 、double 、char * 等。

如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。

想要获取指针指向的数据时,一级指针加一个,二级指针加两个

C语言空指针NULL以及void指针

空指针 NULL

一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。

很多初学者会在无意间对没有初始化的指针进行操作,这是非常危险的,请看下面的例子:

#include <stdio.h>int main(){    char *str;    gets(str);    printf("%s\n", str);    return 0;}

这段程序没有语法错误,能够通过编译和链接,但当用户输入完字符串并按下回车键时就会发生错误,在 Linux 下表现为段错误(Segment Fault),在 Windows 下程序直接崩溃。如果你足够幸运,或者输入的字符串少,也可能不报错,这都是未知的。

前面(《C语言局部变量和全局变量》一节)我们讲过,未初始化的局部变量的值是不确定的,C语言并没有对此作出规定,不同的编译器有不同的实现,我曾警告大家不要直接使用未初始化的局部变量。上面的代码中,str 就是一个未初始化的局部变量,它的值是不确定的,究竟指向哪块内存也是未知的,大多数情况下这块内存没有被分配或者没有读写权限,使用 gets() 函数向它里面写入数据显然是错误的。

我强烈建议对没有初始化的指针赋值为 NULL,例如:
char *str = NULL;

我们在自己定义的函数中也可以进行类似的判断,例如:

void func(char *p){    if(p == NULL){        printf("(null)\n");    }else{        printf("%s\n", p);    }}

这样能够从很大程度上增加程序的健壮性,防止对空指针进行无意义的操作。

其实,NULL 是在stdio.h中定义的一个宏,它的具体内容为:
#define NULL ((void *)0)
(void )0表示把数值 0 强制转换为void 类型,最外层的( )把宏定义的内容括起来,防止发生歧义。从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据。

在进程的虚拟地址空间中,最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。

现在只需要记住,在大多数操作系统中,极小的地址通常不保存数据,也不允许程序访问,NULL 可以指向这段地址区间中的任何一个地址。

注意,C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL 和 0 等同起来,例如下面的写法是不专业的:
int *p = 0;
而应该坚持写为:
int *p = NULL;
注意 NULL 和 NUL 的区别:NULL 表示空指针,是一个宏定义,可以在代码中直接使用。而 NUL 表示字符串的结束标志 ‘\0’,它是ASCII码表中的第 0 个字符。NUL 没有在C语言中定义,仅仅是对 ‘\0’ 的称呼,不能在代码中直接使用。

void 指针

对于空指针 NULL 的宏定义内容,上面只是对((void )0)作了粗略的介绍,这里重点说一下void 的含义。void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。

也就是说,void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。

C语言动态内存分配函数 malloc() 的返回值就是void *类型,在使用时要进行强制类型转换,请看下面的例子:

#include <stdio.h>int main(){    //分配可以保存30个字符的内存,并把返回的指针转换为 char *    char *str = (char *)malloc(sizeof(char) * 30);    gets(str);    printf("%s\n", str);    return 0;}

这里重点是让大家理解void *,它不是空指针的意思,而是实实在在的指针,只是指针指向的内存中不知道保存的是什么类型的数据。


总结:
空指针:指向地址为0的指针(无意义)
void *指针: 指向某个地址的指针(可用),只是该地址存放的数据类型不清楚而已。所以在后续使用时一般需要强制类型转换。

char *str = (char *)malloc(sizeof(char) * 30)

malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。如果无法获得符合要求的内存块,malloc函数会返回NULL指针,

因此在调用malloc动态申请内存块时,一定要进行返回值的判断。

0 0
原创粉丝点击