c语言的几个重要知识点
- 内存结构
这是核心中的核心,请仔细看完,充分理解,否则请不要看下一节内容。
每个程序一启动都有一个大小为4GB的内存,这个内存叫虚拟内存,是概念上的,真正能用到的,只是很小一部分,一般也就是在几百K到几百M。我们PC中内存,我们称之为物理内存,也就是256M,512M等,虚拟内存和物理内存的如何转换是由操作系统完成的,我们不需要管它。我们只需要管好我们自己程序的那4GB内存就可以了。
要管理4GB的虚拟内存,就必须给每个字节分配一个号码,以便程序与访问到其中任何一个字节。这个号码是从0开始顺序递增的,针对于这个号码我们就称之为地址,从0x00000000-0xFFFFFFFF,这样,我们理论上就可以访问其中内存中任何一个字节了。但有一点请注意,系统并不让我们全部都可以用。其中后面2GB的内容是留给系统用的,用户是不可以访问的,而且在前面的2GB也有部分区段不能访问,比如0x00000000就不能访问。具体是哪些区段,不必关心。
注意:类似于0x12345678或12345678H是10进制数305419896的16进制表示法,他们是一回事,显示16进制是为了方便显示及计算机计算。
程序都是用来做一些具体的事情,不管做什么事,结构都是很相似。程序启动,就有4GB的虚拟内存,通过CPU的计算,改变内存的内容,最后再复制内存的内容输出,输出的目的地可以是:屏幕、文件、磁盘等外存、端口、网络等。如何输出呢,最后全部都是调用系统的API,由操作系统完成。(这段话,请仔细体会,并牢牢记住)
所以我们的核心问题就是:如何控制内存,让内存里的值,变成我们想要的结果。
注意:这里的控制,指读取或写入某段内存的内容。
在虚拟内存中,我们一般将其分为4个区域:
栈(stack)
堆(heap)
静态区域(static)
数据区域(data)
注意:不同的资料可能到具体的分法,有所不同,但大体上就是这样,我也是这样理解的。如下图:
有兴趣的话,可以参考《Windows核心编程》第三版,里面有详细的论述。
栈
任何除静态外的变量,数组等。都是被分配到栈中的。这些变量类似于:
int x;
char c;
char s[10];
整个程序中,栈的区域是一个连续的区域,其大小在VC6.0中,是1M。这个栈的特点有点类似于我们以前学过的数据结构课程中的堆栈,都是后进先出。如何理解呢?看下面的程序:
1 #include <stdio.h> 2 3 4 5 void ExecuteOtherCode() 6 7 { 8 9 /* 10 11 ... 12 13 */ 14 15 } 16 17 18 19 void TestStack1() 20 21 { 22 23 int a = 1; 24 25 int b = 2; 26 27 28 29 ExecuteOtherCode(); 30 31 } 32 33 34 35 void TestStack2() 36 37 { 38 39 int c = 3; 40 41 int d = 4; 42 43 ExecuteOtherCode(); 44 45 } 46 47 48 49 void main() 50 51 { 52 53 TestStack1(); 54 55 TestStack2(); 56 57 }
栈的处理在VC6中是从高地址到低地址。执行该程序,运行函数到TestStack1,其中定义一个变量a,此时a就是在栈中分配一段大小为sizeof(int)的内存空间。比如a的地址&a的值就为0x0012ff28,由于a是int型的数据,其占用内存的大小为4B(其详细介绍参见稍后的注意)。所以b地址为0x0012ff24,这两个内存的分配过程我们称之为“入栈”。见下图:
TestStack1结束后,系统则先收回b的空间,再收加a空间,这个过程我们叫“出栈”。即0x0012ff28到0x0012ff28-2*4这段空间的内容不再有用了,即使其值还没有变化。接着再运行到TestStack2函数,也定义了两个int变量c,d,同样进行入栈操作,这样c,d很“可能”就占用了原来a,b的空间,见下图:
这里用到“可能”两个字,是因为实际栈中存放着不仅仅是这些变量,包括函数的指针等也是存放在栈中,这样就会造成两个类似函数中的变量所占据的内存空间不一样。
注意:计算占用内存空间的大小,可以用sizeof(x)表示,其中x可以是变量,指针,数组,以及各种类型名等,其返回值为整形数值。每一种类型占用多大空间,这个要特别注意,在我们平常的32位普通PC机中,常见的有:
char 1B
short 2B
int 4B
long 4B
float 4B
double 8B
任何类型的指针均为 4B,它正好能指向全部4G的虚拟内存,2的32次方为4G。
数组:
int arr[10];
表示10个连续的int类型的内存区域。
则sizeof(arr)的值为10* sizeof(int),就是40B
这些内置的int等类型,默认都是指有符号的,即可以赋值为负数。如果是定义成无符号的,如
unsigned int x;
则sizeof(x)还是4B,有无符号,在占用内存空间的大小上是一样的。其它的也是如此。
结构
struct A
{
int x;
int y;
};
A mystruct;
表示声明有一个结构类型A,其中有两个int类型的成员,定义该类型一个对象mystruct。则
sizeof(mystruct)就是sizeof(mystruct.x)+sizeof(mystruct y),即sizeof(int) + sizeof(int),即8B
这些类型的大小,对理解内存中数据结构,很有帮助,请记住。
栈有入栈和出栈,当程序运行到一个函数中,依次将函数中定义的变量入栈,运行完该函数,然后按相反的顺序出栈,这些都是系统自动完成的,我们不用管,知道原理就行了。所以这些变量都是临时,一量出了函数,我们都不能用它。事实上,差不多一切临时的数据都被分配到栈中。
堆
堆是相对于栈的,前面说到栈的大小大概为1MB,而用户能用到的内存大概有2GB,因此除了少量的数据区域和静态区域,以及这2G中被小部分限制的区域处,剩余都是堆的空间,其大小还是接近2G。(接近2G,是我的理解,没有在其它书中看到类似的结论,说错了,你别笑话我。)
堆主要有两个作用:
1、 欲分配内存空间的大小,或称长度,可以是变量,这意味着这个大小,在分配前,可以随着环境的改变而改变,不要求是定值。注意,这里说的是“分配前”,在分配完了以后,这段内存的大小理论上还是不可以改大小的,除非释放掉或用一些特别的方法。
2、 可以分配比较大的空间。
堆分配内存,主要通过函数malloc(),释放用free()。比如:
#include <stdlib.h> /* 要加上该头文件 */
int size = 10*sizeof(int);
int* pInt = (int*)malloc(size);
表示在堆中,分配长度为size的内存,将分配到的那段内存,标识成一系列int型数据,并将这段内存的地址,赋值给一个int型指针pInt,这样,通过pInt就可以控制这段内存了。
我们知道在栈中,可以通过数组,也能达上述的效果,如下:
int arr[10];
他们是有区别的:
1、 在效率上,前者(指pInt那段内存)是通过系统在整个堆空间中搜索到一段合适的内存,然后把这段内存分配给pInt,而后者只是在临近的位置分配这样大小的内存,这样少了搜索过程,后者显示效率高得多。
2、 在大小上,前者总共能分配的内存接近2GB,可以说很大了,而后者,其栈总的大小才1MB,所以其分配的大小不可能超过1MB,确切来说,是不能超过1M-分配前栈的大小。换句话说,如果要分配的空间超过1M的话,只能选择前者。
3、 在作用范围上,前者的内存地址可以用一个指针表示,假如这个指针是全局变量的话,则一直可以控制这块内存了,事实上,只要这块内存不被释放,那么在程序任何地方,它只要知道该内存的地址,则可以控制它。后者在退出函数或其作用域后,该段内存就被收回了。
4、 在“3、”中,似乎感觉堆很好,但隐藏了一个重要的麻烦,那就是内存释放,因为堆的内存释放是需要手工进行的。如果一不小心,用完后,我忘记释放,那么结果会怎样呢?事实上这段内存则不会被再用到,直到程序结束,比如我申请了200M的内存,没释放,这种浪费还是很可观的。
针对于他们的区别,我们可以有一个结论:
同时满足:临时性,小的(不超过1M),更快的内存分配用栈,(其实第3条“更快”由于现代机器够快,两种方式都会满足),否则用堆。
这些是栈和堆的区别,在实际工作中非常重要,请充分理解,不理解的话就背下来,这在面试时会经常考到。
如果堆分配不成功的话,则返回NULL。
前面说过,针对于某段在堆中分配的内存,如果不再需要使用了,则应该释放。这个释放用free();
如下:
int* pInt = (int*)malloc(size);
/* 针对于pInt做一些操作 */
free(pInt);
静态区域:
该区域主要存储全局变量和静态变量。该区域内存储的变量在程序的整个运行期间都存在,不会像栈那样,运行完某个函数,该函数内定义的普通变量就被弹出栈,其地址空间就会被收回,也不像那堆那样,用free就被系统收回。
所以定义了一个全局变量或静态变量,其存储在内存中那块区域,是自程序启动到程序结束前都是固定的,不会有别的变量占用这块内存。
全局变量:只要在任何一个函数外声明的变量就是全局变量,在任何地方都可以被访问到。
静态变量:
声明类似于如下:
static int static_value = 0;
1、 最前面一定要static,表明它是静态变量.
2、 一定要赋值初值,实际上这个赋值过程是在程序一启动时,就运行了,而不是在进入到某个函数时,才执行。
其实我们可以这样认为:静态变量就是全局变量,只是其访问的范围比较有限,只能在定义它的函数中访问。见如下程序:
1 void TestStatic() 2 3 { 4 5 static int i=5; 6 7 i++; 8 9 } 10 11 12 13 void main() 14 15 { 16 17 TestStatic(); 18 19 TestStatic(); 20 21 }
运行第一个TestStatic时。
其变化如图所示:
运行第二个TestStatic()时,如图所示:
数据区域:
一些存储常量的地方:比如:
char* p = “abcdefg”;
这里p也是有值的,不过一般都不会这么写。正确的方法是
char p[] = “abcdefg”;
常用操作内存的库函数:
要控制内存,主要有复制、设值、比较等操作,共对应库函数如下:
1、复制
void *memcpy( void *dest, const void *src, size_t count );
表示将地址为src,长度为count的一段内存上的内容,复制到地址为dest开始的一段内存中。
2、比较
int memcmp( const void *buf1, const void *buf2, size_t count );
表示将以buf1开始的内存和以buf2开始的两段内存相互比较,要比较的长度为count,返回值>0,表示buf1大,<0表示buf2的,==0,则表示这两段内存的内存相同。
1 #include <string.h> 2 #include <stdio.h> 3 int main( void ){ 4 char first[] = "12345678901234567890"; 5 char second[] = "12345678901234567891"; 6 int int_arr1[] = {1,2,3,4}; 7 int int_arr2[] = {1,2,3,4}; 8 int result; 9 printf( "Compare '%.19s' to '%.19s':\n", first, second ); 10 result = memcmp( first, second, 19 ); 11 if( result < 0 ) 12 printf( "First is less than second.\n" ); 13 else if( result == 0 ) 14 printf( "First is equal to second.\n" ); 15 else printf( "First is greater than second.\n" ); 16 printf( "Compare '%d,%d' to '%d,%d':\n", int_arr1[0], int_arr1[1], int_arr2[0], int_arr2[1]); 17 result = memcmp( int_arr1, int_arr2, sizeof(int) * 2 ); 18 if( result < 0 ) 19 printf( "int_arr1 is less than int_arr2.\n" ); 20 else if( result == 0 ) 21 printf( "int_arr1 is equal to int_arr2.\n" ); 22 else printf( "int_arr1 is greater than int_arr2.\n" ); 23 }
3、设值
void *memset( void *dest, int c, size_t count );
表示将内存dest,大小为count的一段内存的每个字节,全部赋值为c。一般来说,该函数用来清0。即
memset(buffer, 0, buffer_size);
这几个库函数在memory.h中声明,如果编译器提示找不到这些函数,则要include它。
内存结构知道这么多,已经差不多了。这部分内容,请仔细体会。
- 指针
前面已经说过,内存都有地址编号的,这个地址编号是从0x00000000到0xFFFFFFFF,这样我们知道了某一个地址就可以访问这段内存,以此思路,如果我们定义了一个整型变量,再将一个具体的地址编号赋值给它,是否利用这个整型变量就可以访问该段内存呢?
答案是肯定的,但一般我们不这样做,在C语言中,引进了一个专业名词:指针
指针有两个方面的属性:
1、 指针的值就是地址编号。
2、 指针是有类型的。
对于1、的理解,你可以把指针想像成整型变量,事实上,指针的确可以和整型变量互换,比如:
char* p = (char*)(5);
int i = (int)p;
如图:
但不建议你这样做,除非,对内存已经很熟了,事实上开始地址为0x00000005的一段内存是不可以被访问的。
对于2、的理解,我们可以这样认为:描述一段内存,则必须指明其开始地址,及长度。这个指针类型,就是表示这段内存有多长,及这段内存到底是干什么用的,比如:
int vlaue = 1234;
int* p = &value;
可以这样理解p:它就是指向value所在那段内存,长度为sizeof(int),这段内存就表示一个int类型的值。
既然指针同整型变量类似,那么指针本身所占内存也是可以确定的,它的值范围在0-4G,所以用4B就够了。的确,在C语言中,32位普通PC上,任何指针都是大小都是4,比如:
char* p1;
int* p2;
struct A
{
int x;
int y;
}
A* p4;
那么
sizeof(p1)、sizeof(p2)、sizeof(p3)的值都是4B,
但
sizeof(*p1)、sizeof(*p2)、sizeof(*p3)的值应该等同于
sizeof(char)、sizeof(int)、sizeof(struct A) /* 结构体类型应用时,要在其前面加struct */
即 1, 4, 8
这里有一点请注意:
结构A的大小,一般认为就是其成员x,y的大小之和。但有时候情况比较特殊,如
struct B
{
int x;
int y;
char z;
};
此时
sizeof(B)不是9,而是12,这是由于编译器为了考虑效率的问题,一般要将内存对齐,所以一个类型的大小通常是4或8的整数倍。这种内存对齐机制可以在编译器中设置。
这种对齐机制不要求掌握,知道有这么回事就行,面试时有可能会被问到。所以提一下。
看到这里,你应该能理解,指针其实就是一个变量,只是这个变量的值就是内存地址而已。我们知道普通变量是保存到栈中的,那么指针本身也将是保存到栈中,也是可以取地址的,甚至其地址也可以被赋值给另一个指针,这称之为指向指针的指针,比较有趣吧。
比如:
void main()
{
int i=5;
int* p = &i;
int** p2 = &p;
}
由此也可以体会指针和引用的区别
在栈中定义一个变量i,比如其地址为0x0012ff7c,大小为sizeof(int)(即4B),那么其下一个“变量”p的地址就是0x0012ff7c-4即0x0012ff78,只是这个地址存放着一个指针,该指针的值为0x0012ff7c,下面又是一个指针,其值为0x0012ff78。如图:
对指针的理解就是,指针就是指向内存地址,这块内存存放的内容可以是任何东西,甚至还是一个指针。
当然指向指针的指针是一个相对比较高级的话题,不理解也没关系。
前面说到,指针是有类型的,但有的时候,我们不知道其类型是什么,或者只要求知道开始的内存地址就行了,可以这样定义指针:
void* p;
表示有一个指针,它可以指向某段内存,但不知这段内存具体表示什么,也不知道其长度,即:
sizeof(*p)是无效的。
如果想要其长度,则应该另外再带上一个变量。前面的对内存操作的库函数,就是属于这种机制。
针对void*的指针有这样的特点:
void*类型的指针,可以被任何其它类型的指针赋值,比如:
int i = 5;
int* p1 = &i;
void* p2 = p1;
注意:
1、void如用于函数定义时,表示如下:
void fun(void)
{
/*
…
*/
}
表示有一个函数,没有参数,也没有返回值。其中表示没有参数时,也可以省略void。
2、因为void没有类型,不知道占用空间的大小,所以不能定义成变量,只能作为指针的类型。
我们是否可以引申一下,要定义成变量的,其类型必须明确大小。(这是我自己的理解,未在其它书中看到类似的结论)
- 字符串处理
这是一个大家经常遇到,但又似懂非懂的主题。这里面有几个概念如下:
(1)、字符变量
(2)、字符指针
(3)、字符数组
(4)、字符串
电脑是外国人发明的,开始时他们需要显示在屏幕上字符只是字母、数字及一些标点符号,把这些字符全部加在一起,也只有100来个(哪有我们汉字,这样博大精深啊)。由于计算机都由二进制数字组成,这样,他们就规定类似于0代表A,1代表B…之类的编码规则。后来这个编码规则形成一个统一的标准,我们称之为ASCII码。
ASCII表格:
这样,字符就是和0x00-0xFF中的整数值相应,所以在C语言中,干脆把两者看成一回事。
int i = ‘a’;
char c = 0x41;
这都没问题,i的值为0x61,c的值是’A’,之所以可以这样做,是因为它们的存储方式都是一样的。如图:
既然字符和0到127的数据是一回事,那么
1 + ‘2’的值为’3’
‘a’ + 3的值为’d’
也应该好理解了。
一个有符号位的字节能表示的大小为-128-127,所以在ASCII中,用一个字节就可以表示字符了。也就是说一个ASCII码的字符占用内存空间为1B。
一般处理字符,不可能是单个字符,而是多个字符的组合,在C语言中,称之为字符串。字符串有许多字符组成,但到底有多少个呢?在C中规定,字符串是以0结尾的,这个0是指ASCII值为0的那个字符,即NULL。比如,
字符串: “abc” 在内存中就是这样存储
61 62 63 00 /* 这里的值都是16进制 */
字符串,是许多字符连续排序在一起的。因此,可以用数组表示:
char buffer[256];
这表示定义了一个256个元素的数组,元素的类型为char型,所以该数组占用空间为256*sizeof(char),即256B
这称之为字符数组。有时定义时,可以初使化一下:
char buffer[256] = {0};
表示将buffer中每个元素的值都设置为0。
char buffer[] = “abc”;
表示将字符串”abc”连同结束符0,都赋值给buffer,即此时,sizeof(buffer)为4B
对字符串的处理,有时需要定位于一个具体的字符,用相应的指针可能更灵活,于是可以定义一个字符指针:
char* p = buffer;
表示定义一个字符指针,让其指向数组buffer的地址。
字符指针除了方便定位于每一个字符外,还有一个用途是,可以很方便地作为函数参数传递。
处理字符串主要用的函数有:
size_t strlen( const char *string );
表示取字符串的长度,即字符数
注意:
const:表示不改变此字符串的内容。
函数认为string是以0结尾的。
size_t不同的编译器定义可能不一样,但一般都是unsigned int类型。
有如下程序:
char str = “abc”;
sizeof(str)的值为4,见前面说明。
但strlen(str)为3,说明strlen计算时,不包括结束符0
char *strcpy( char *strDestination, const char *strSource );
将strSource字符串的内容复制到strDestination中去
注意:该函数假定字符串strDestination有足够的空间容纳strSource中的内容,否则可能会覆盖其后的内容。所以使用时要注意。
int strcmp( const char *string1, const char *string2 );
表示比较两个字符串是否相等,如果返回值 >0,则string1大,<0,则string2大,==0 则相等
由于字符串都处于内存中,所以对字符串的赋值及比较操作,完全可以用内存操作的函数,如memcpy, memcmp等代替。
单个字符的输出,可以这样
char c = ‘a’;
printf (“%c”, c);
字符串的输出,可以这样
char str[] = “abc”;
printf (“%s”, s);
要将一个整数转换成字符串,可以这样
char str[64];
int i = 100;
sprintf(str , “%d”, i);
反过来,要将字符串转换成整型,有函数atoi,如下:
i = atoi(str);
- 链表
链表都是类比于数组的。
数组在内存空间每个元素的位置都是连续的,即&a[i]+sizeof(a[i])必定与&a[i+1]相等。
比如,有任意4个数, 3, 5, 9, 2。用数组可以表示如下:
int a[4] = {3, 5, 9, 2};
这样表示有几点局限性:
1、 很明显,这是定义在栈中,前面讨论过栈最大不超过1M,如果数量很多,以致于总的需要空间超过1M,就不行了。
2、 栈中不行,可以定义在堆中,但有一点,如果是元素数量很多的数组,我要在其中某个位置插入一个元素,则插入点后面的元素都应该相应后移一位,这造成效率上的降低。
3、 不方便增加一个元素,这里定义了4个元素,如果我还要增加1个元素,这种方式则难办了。
4、 即使我事先定义一个元素数量比较大的数组,实际用到的元素可能是不确定的,这样将会造成大量内存浪费。
数组在内存存储的位置,见图:
为了解决以上问题,我们可以引入链表。
在数组a中,a[1]一定是紧接排列在a[0]的后面。前面已经说过,所有的元素紧密地排一起不一定是好事,那就分开吧。这样我们就可以令a[1]不紧靠着a[0],但这又引入了一问题,以前大家做邻居的时候,拜访完a[0],找a[1]很方便,现在a[1]搬走了,怎么办?留个QQ号或地址什么的总可以吧。留给谁,当然是a[0]了。同样,a[2]也不跟a[1]做邻居了,自然也要留下地址。这样,每个元素就有两个属性了:
1、 本身的值
2、 找到下一个元素的线索
根据这样两个属性,我们可以这样定义元素
struct node
{
int data;
struct node * next; /* 这种定义方法,表示定义一个指针,其指向一个node类型的
结构体 */
};
前面说过,a[1]的地址由a[0]保存,那a[0]的地址由谁保存,答案是没人给他保存,所以必须记下来,我们可以用一个头指针记录元素a[0]的地址,即定义一个指针:
struct node* pHead=NULL;
那么以后,只要知道,这个pHead,就可以找到所有的元素了。
现在可以创建各个元素了
struct node* CreateNode(int data)
{
struct node* p = (struct node*)malloc(sizeof(struct node));
p->data = data;
p->next = NULL;
return p;
}
接下来,就是形成链表了,这里简单点,比如数据,已经存放在前面那个数组a中了。
node* pNode=pHead; /* 记录每一个新产生的node */
node* pTemp; /* 临时性的 */
for (int i=0; i<4; i++)
{
pTemp = CreateNode(a[i]);
if (pNode == NULL)
{ /* 第一次 */
pHead = pTemp;
}
else
{
pNode->next = pTemp;
}
pNode = pTemp;
}
到此链表组建完成,如图:
以后要遍历整个链表,可以如下:
pNode = pHead;
while(pNode != NULL)
{
printf (“%d”, pNode->data);
pNode = pNode->next;
}
要在某个node之后,插入一个元素,如下
void Insert(struct node* pNode, int new_data)
{
struct node* p = CreateNode(new_data);
struct node* pTemp = pNode->next;
pNode->next = p;
p->next = pTemp;
}
如图:
完整的例子代码如下:
1 #include <stdio.h> 2 3 #include <stdlib.h> 4 5 6 7 /* 定义一个结点 */ 8 9 struct node 10 11 { 12 13 int data; 14 15 struct node* next; 16 17 }; 18 19 20 21 /* 创建一个结点 */ 22 23 struct node* CreateNode(int data) 24 25 { 26 27 struct node* p = (struct node*)malloc(sizeof(struct node)); 28 29 p->data = data; 30 31 p->next = NULL; 32 33 return p; 34 35 } 36 37 38 39 void main() 40 41 { 42 43 struct node* pHead = NULL; /* 头指针,开始时为NULL */ 44 45 struct node* pNode = pHead; /* 指向每个结点 */ 46 47 struct node* pTemp; 48 49 int data; 50 51 52 53 /* 建立链表 */ 54 55 while (1) 56 57 { 58 59 scanf("%d", &data); /* 输入整数,0 则表示结束 */ 60 61 if (data == 0) 62 63 break; 64 65 66 67 pTemp = CreateNode(data); 68 69 if (pNode == NULL) 70 71 { 72 73 pHead = pTemp; 74 75 } 76 77 else 78 79 { 80 81 pNode->next = pTemp; 82 83 } 84 85 86 87 pNode = pTemp; 88 89 } 90 91 92 93 /* 遍历整个链表 */ 94 95 pNode = pHead; 96 97 while(pNode != NULL) 98 99 { 100 101 printf ("%d ", pNode->data); 102 103 pNode = pNode->next; 104 105 } 106 107 108 109 /* 由于链表每个结点通过malloc创建,结束时,别忘记了free */ 110 111 while(pNode != NULL) 112 113 { 114 115 pTemp = pNode->next; 116 117 free(pNode); 118 119 120 121 pNode = pTemp; 122 123 } 124 125 } 126 127 128 129 该例子代码,应该与复习大纲中的建立单链表功能一致。请把该段代码仔细理解,否则不要往下面看。 130 131 复习大纲中的代码及补充代码如下: 132 133 #include <stdio.h> 134 135 #include <stdlib.h> 136 137 struct link 138 139 { 140 141 int data; 142 143 struct link *next; 144 145 }; 146 147 int n; /* 统计结点总数 */ 148 149 150 151 /* 创建整个链表 */ 152 153 struct link *creat( ) 154 155 { 156 157 /* head表示链表的头,将会作为返回值 */ 158 159 /* p1, p2都表示指向某个结点 */ 160 161 struct link *head, *p1, *p2; 162 163 164 165 n=0; /* 初使值为0 */ 166 167 168 169 /* 要求用户输入第一个值 */ 170 171 p1=p2=(struct link *)malloc (sizeof(struct link)); 172 173 scanf("%d",&p1->data); 174 175 176 177 /* 注意:此处是程序流程的一个BUG,如果第一次输入的p1->data为0的话, 178 179 就直接返回head了,此时head为NULL,所以第一次malloc得到内存将无法释放, 180 181 形成内存泄漏,但如果仅是演示,则无关大雅*/ 182 183 184 185 head=NULL; 186 187 while(p1->data!=0) /* 以输入是否为0作为判断条件 */ 188 189 { 190 191 n=n+1; /* 结点数加1 */ 192 193 if (n==1) /* 第一次,则在头中,进行记录 */ 194 195 head=p1; 196 197 else /* 前一个结点中的指针,指向当前结点 */ 198 199 p2->next=p1; 200 201 202 203 p2=p1; /* 跳过当前结点 */ 204 205 206 207 /* 继续要求用户输入 */ 208 209 p1=(struct link *)malloc (sizeof(struct link)); 210 211 scanf("%d",&p1->data); 212 213 } 214 215 216 217 /* 最后一个结点中的指针,要求为NULL */ 218 219 p2->next=NULL; 220 221 222 223 return(head); 224 225 } 226 227 228 229 void main() 230 231 { 232 233 struct link *head, *p, *temp; 234 235 head = creat(); 236 237 238 239 /* 遍历 */ 240 241 p = head; 242 243 while (p != NULL) 244 245 { 246 247 printf("%d ", p->data); 248 249 p = p->next; 250 251 } 252 253 printf ("\nTotle: %d\n", n); 254 255 256 257 /* 释放 */ 258 259 p = head; 260 261 while (p != NULL) 262 263 { 264 265 temp = p->next; 266 267 free(p); 268 269 p = temp; 270 271 } 272 273 } 274 275 276 277 大纲中合并两个有序链表的说明如下: 278 279 280 281 struct lk 282 283 { 284 285 int data; 286 287 struct lk *next; 288 289 } 290 291 292 293 /* 将两个本身为升序的链表,合并后成为一个新的链表 */ 294 295 struct lk *merge(a,b) 296 297 struct lk *a, *b; /* 参数的表示方式,相当于 298 299 struct lk* merge(struct lk* a, struct lk*b) */ 300 301 { 302 303 struct lk *c, *p; 304 305 306 307 /* 比较a,b两链表的第一个结点,取出其值小一点的结点 */ 308 309 if (a->data < b->data) 310 311 { 312 313 c=a; 314 315 a=a->next; 316 317 } 318 319 else 320 321 { 322 323 c=b; 324 325 b=b->next; 326 327 } 328 329 330 331 p=c; 332 333 334 335 while (a!=NULL && b!=NULL) 336 337 { 338 339 /* 依次比较a,b中每一个结点 */ 340 341 if (a->data < b->data) 342 343 { 344 345 p->next = a; 346 347 a = a->next; 348 349 } 350 351 else 352 353 { 354 355 p->next = b; 356 357 b = b->next; 358 359 } 360 361 362 363 p = p->next; 364 365 } 366 367 368 369 /* 如果a,b中有哪一个提前遍历完,则另一个剩余的部分,就被挂在新的链表中末尾 */ 370 371 if (a == NULL) 372 373 p->next=b; 374 375 else 376 377 p->next=a; 378 379 380 381 /* 返回新生成的链表头 */ 382 383 return (c); 384 385 }
- 复习大纲中的一些知识点理解
1、
如何理解当a = 2时
a + = a - = a * a 结果为 - 4
a + = a - = a * =a 结果为 0
a /= a + a 结果为 0
解答:
针对于 a += a -= a * a ; 相当于 a += (a -= (a*a));
1、初值 a 的值为 2;
2、计算: a * a ,==〉 2*2,结果为4;
3、计算: a -= 4,==〉 a = a-4 ==〉 a = 2-4,结果为-2,此时 a 的值为 -2;
4、计算: a += -2,==〉 a = a+2 ==〉 a = -2 + (-2),结果为-4,此时 a 的值为 -4;
针对于 a += a -= a *=a ; 相当于a += (a -= (a *=a));
1、初值 a 的值为 -4; (这里假设该式是在前面的式子运行完后,接着运行,但后来感觉,该式可能是独立的,与前面的式子无关,无论初值如何,不影响理解)
2、计算:a *= a,==〉 a = a*a ==〉 a= (-4)*(-4),结果为16,此时 a 的值为 16;
3、计算:a -= 16 ==〉a = a-16,结果为0,此时 a 的值为 0;
4、计算:a += a; ==〉 a = a+a,结果为0, 此时 a 的值为 0; 注意: a += (a -= (a *=a)); 这种式子,恒为0。原因在于 a -= (a*=某个值); 相当于a减去自身,只不过在进行减去之前,先自我变化了一番,但不管怎么变化,都是减去自身,所以恒为0。
针对于 a /= a+a ; 相当于 a /= (a+a);
1、初值 a 的值为2;
2、计算: a+a,结果为4;此时a的值仍为2;
3、计算:a /= 4; ==> a = a / 4; ==> a = 2 / 4; ==> a = 0; 结果为0;
2、冒泡排序
源程序解释如下:
#include <stdio.h>
#define SIZE 10
/* 交换两数 */
void swap(int *p1, int *p2)
{
int temp;
temp=*p1;
*p1=*p2;
*p2=temp;
}
/* 冒泡排序的算法 */
void sort(int *array, int n)
{
int i, j;
/*
这段循环可以这样理解:
假设n为10,则有a[0]到a[9]
第1步,将[0,9]中最大数,沉到a[9]中
第2步,将[0,8]中最大数,沉到a[8]中
...
第n步,将[0,0]中最大数,沉到a[0]中
*/
for (i=1; i<=n-1; i++)
for (j=0; j<=n-i-1; j++)
{
if(array[j] > array[j+1]) /* > 表示升序,否则表示降序 */
swap(&array[j], &array[j+1]);
}
/* 我喜欢这样写,便于记忆 */
/*
for (i=0; i<n; i++)
for (j=0; j<i; j++)
{
if(array[j] > array[j+1])
swap(&array[j], &array[j+1]);
}
*/
}
main()
{
int i, a[SIZE];
/* 输入10个数 */
for (i=0; i<=SIZE-1; i++)
scanf("%d", a+i);
/* 测试 */
/*
a[0] = 10;
a[1] = 70;
a[2] = 20;
a[3] = 14;
a[4] = 60;
a[5] = 19;
a[6] = 20;
a[7] = 256;
a[8] = 23;
a[9] = 86;
*/
sort(a, SIZE);
/* 输出 */
for (i=0; i<=SIZE-1; i++)
printf("%d\n", a[i]);
}
1、 关于静态变量
其可运行源码如下:
#include <stdio.h>
long f(n)
int n;
{
static x=2;
if (n==0)
return (1);
else
{
x=x*n;
return(x);
}
}
main()
{
/*
printf("%d\n",f(0)+f(1)+f(2)+f(3));
等同于下面式子
*/
int f0, f1, f2, f3;
f0 = f(0); /* 运行前:x为2,运行后:x为2,f0为1 */
f1 = f(1); /* 运行前:x为2,运行后:x为2,f0为2 */
f2 = f(2); /* 运行前:x为2,运行后:x为4,f0为4 */
f3 = f(3); /* 运行前:x为4,运行后:x为12,f0为12 */
printf("%d\n",f0+f1+f2+f3);
}
2、 关于变量作用域,其源码如下:
#include <stdio.h>
int x=4;
void a();
void b();
void c();
void d(x);
main()
{
int x=6; /* 此处,内部的x将外部的x给屏蔽掉了 */
printf("x in main is %d\n", x); /* x为6 */
a();
b();
c();
d(x); /* 相当于d(6) */
x++; /* 运行后x为7 */
a();
b();
c();
d(x); /* 相当于d(7) */
printf("x in main is %d\n", x); /* x为7 */
}
void a()
{
int x=25; /* 此处,内部的x将外部的x给屏蔽掉了 */
printf("x in a is %d\n", x); /* x永远为25 */
}
void b()
{
static int x=50; /* 此处,内部的x将外部的x给屏蔽掉了
并且是静态的,所以这句话,在程序启动时
就已经运行了,而不是到运行函数时再运行的。
*/
printf("x in b is %d\n", x++); /* 先取x,再将x自增,这个x在下次运行该函数时,是有用的 */
}
void c()
{
x *=10; /* 用的全局变量 */
printf("x in c is %d\n", x);
}
void d(int x)
{
printf("x in d is %d\n", ++x); /* 用的参数x,先将其自增,再取值 */
/*
相当于
x += 1;
printf("x in d is %d\n", x);
*/
}
3、 关于&& ||及++问题
(1)、C语言中无bool型的变量,其思想用整数代替
0 表示false,同时false用0表示
非0表示true,同时true用1表示,比如关系,比较运行符返回的结果。
如 :
if (2)
{
printf (“true”);
}
else
{
printf (“false”);
}
将会输出true
(2)、||及&&均从左往右计算
0 && 任何表达式,结果为0。其中任何表达式将不再计算。
非0 ||任何表达式,结果为1。其中任何表达式将不再计算。
(3)、++a: 表示先自增,再取值
a++: 表示先取值,再自增
总之,什么在前面,就先做什么。
如:
int a = 10;
int b = ++a; /* 此时 a 为11,b为11 */
a = 10;
int c = a++; /* 此时 a 为11,c为10 */
大纲中可运行程序如下:
#include <stdio.h>
void main()
{
int a,b,c,d,m,n, result;
a = 1;
b = 2;
c = 2;
d = 4;
m = 1;
n = 1;
result = (m = a>b) && (n = c>d);
/*
1、执行: m = a>b ==> m = 1>2 ==> m=0
2、执行: 0 && (n = c>d),其结果为0,则(n = c>d)不执行,
所以 n 仍为1;
*/
printf ("m = %d, n = %d\n", m, n);
a = 3;
b = 5;
result = a++ || b++;
/*
先取a,再将其加1,==> 3 || b++ 其结果为1,所以 b++ 不执行
此时a为4,b为5
*/
printf ("a = %d, b = %d\n", a, b);
a = 0;
b = 5;
result = a++ && b++;
/*
先取a,再将其加1,==> 0 && b++ 其结果为0,所以 b++ 不执行
此时a为1,b为5
*/
printf ("a = %d, b = %d\n", a, b);
}
其输出:
m = 0, n = 1
a = 4, b = 5
a = 1, b = 5