版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、c企业要求与实践(讲义)第1章c语言基础1.1 main函数的参数与返回值1.1.1 main的参数命令行界而的程序,通常都需要输入命令行参数帮助程序执行。假定冇一个 可执行程序名为test。那么运行该程序的的命令行如2test带命令行参数是同二亍小的附加项:test - c test其屮-c和test就是命令行参数。c程序可以将这些附加参数读出来,并 为自己所用,比如作为程序运行的条件(经常看到调试参数-d就是这么一个)。 c程序通过使用main()的参数来读取这些附加参数,下而的repeat, c给出一个 读出main参数的例子:例.1:include stdio.h>include
2、 <stdlib.h>int main(int argc, char argv)int count;printf (z/the command line has %d argumcnts:n", argc - 1);for (count = 1; count < argc; count+)printf(d: %sn,count, argvcount);printf(n);/system("pause”); return 0;这里先解释一下main(int argc, char*argv)这个函数屮两个参数的意义, argc记录的是命令行中输入参数的数目,a
3、rgv是一个拥有argc个元素的字符串 数组,每个元素保存一个命令行小输入的参数。编译这个文件为可执行文件repeat:gee repeat c -o repeat按下列方式执行repeat程序/repeat i "love you" 3输出如卜:the command line has 3 arguments:1:12: love you3:3在这个例子中,argc的值为4,命令行一共输入了四个参数“./repeat”、 “i”、“love you”、“3”。在dos和unix环境下,命令行参数中用” ”符 号表示其是一个字符串,视为一个参数。1.1.2 main函数的返
4、回值main函数的返回值用于说明程序的退出状态。如果返回0,则代表程序正常 退出;返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退岀。 下而我们在winxp环境下做一个小实验。首先编译下而的程序:int main( void )return 0;然后打开附件里的“命令提示符”,在命令行里运行刚才编译好的可执行文件, 然后输入“ echo % errorlevel %",回车,就可以看到程序的返回值为0。假设刚方编译好的文件是a.exe,如果输入“a && dir”,则会列出当前口录 下的文件夹和文件。但是如果改成“return-1",或者别的非0
5、值,重新编译后输 入“a&& dir”,则dir不会执行。因为&&的含义是:如果&&前而的程序正常退 出,则继续执行&&后面的程序,否则不执行。也就是说,利用程序的返回值, 我们可以控制要不要执行下一个程序。这就是int main的好处。如果你有兴趣, 也可以把main函数的返回值类型改成非int类型(如float),重新编译后执行“a && dir",看看会出现什么情况,想想为什么会岀现那样的情况。顺便提一下, 如果输入a ii dir的话,则表示如果a异常退出,则执行dir。1.2类型转换隐式转换隐式
6、转换就是编译器在自动帮助程序员做的类型转换工作,既然是编译器自 动转换,那么这种转换必须貝有足够的安全性,这是编译器的责任。double dl = 100;int il = 100;double d2 = i 1;这种转换是安全的,double类型占据的内存字节数比int类型的要大,而且 double类型能表示的整数范围也比int人,因此int类型到double类型的转换是 安全的,没有精度损失的,上例中的转换是隐式的,由编译器自动完成的。强制转换强制转换是冇能导致安全问题的,如果占据内存字节数较多的类型转换为占 据内存字节数较少的类型转换,那么就有可能造成精度的损失。double d3 =1
7、.25e+20;double dl = 10.25;int i2 = (int)d3;int i3 = (int)d4;i2为d3转换为int类型的结果,double类型转换为int类型属于大转小,可 能会损失精度(13数字很大,i2的类型无法存储如此大的数字,将会溢出。1.3选择结构1.3.1 if 分支if语句是c+/c语言中最简单、最常用的语句,然而很多程序员用隐含错误 的方式写if语句。木节以“与零值比较”为例,展开讨论。布尔变量与零值比较不可将布尔变量直接与true、false或者1、0进行比较。根据布尔类型的语义,零值为“假”(记为false),任何非零值都是“真”(记为true)
8、o true的值究竟是什么并没冇统一的标准。例如visual c+将 true定义为1,而visual basic则将true定义为1。假设布尔变量名字为flag,它与零值比较的标准讦语句如下:if (flag) / 表示 flag 为真if (!flag) / 表示 flag 为假 其它的用法都属于不良风格,例如:if (flag = true)if (flag = 1 )if (flag 二二 false)if (flag 二二 0)整型变量与零值比较应当将整型变量用“二二”或“!二”直接与0比较。假设整型变量的名字为value,它与零值比较的标准if语句如2if (value = 0)i
9、f (value != 0)不口j模仿布尔变量的风格而写成if (value) /会让人误解value是布尔变量if (lvalue)浮点变量与零值比较不可将浮点变量用或“!二”与任何数字比较。千万要留意,无论是float还是double类型的变量,都有精度限制。所以 一定要避免将浮点变量用或“!二”与数字比较,应该设法转化成“二” 或y”形式。假设浮点变量的名字为x,应当将if (x二二0. 0) 隐含错误的比较转化为if (x二-epsin0m) && (x二epsin0m)其中epsinon是允许的误差(即精度)。指针变量与零值比较应当将指针变量用“二二”或“!二”与nu
10、ll比较。指针变量的零值是“空”(记为null)。尽管null的值与0相同,但是两者 意义不同。假设指针变量的名字为p,它与零值比较的标准if语句如2 if (p = null) / p与nuix显式比较,强调p是指针变量 if (p != null)不要写成if(p = 0)/容易让人误解p是整型变量if (p !二 0)或者if (p)/容易让人误解p是布尔变量if (!p)对if语句的补充说明有时候我们可能会看到if (null = p)这样古怪的格式。不是程序写错了, 是程序员为了防止将if (p = null)误写成if (p = null),而有意把p和 null颠倒。编译器认为i
11、f (p二null)是合法的,但是会指出if (null二p) 是错误的,因为null不能被赋值。程序中有时会遇到if/else/return的组合,应该将如下不良风格的程序 if (condition)return x;return y;改写为if (condition)return x;el sereturn y;或者改写成更加简练的return (condition ? x : y);在使用运算符&&的表达式,耍尽量把最有可能为false的子表达式放在&& 的左边;同样在使用运算符ii的表达式,要尽量把最有可能为true的子表达 式放在ii的左边。因为c语
12、言对逻辑表达式的判断采取“突然死亡法”;如果 &&左边的子表达式计算结果为false,则整个表达式就为false,后面的字表 达式没有必要再计算;如果ii左边的子表达式计算结果为true,则整个表达 式就为true,因此后面的子表达式没有必要再计算。者可以提高程序的执行效 率。1.3.2 switch 分支有了辻语句为什么述要switch语句?switch是多分支选择语句,而if语句只冇两个分支可供选择。虽然可以用 嵌套的if语句来实现多分支选择,但那样的程序冗长难读。这是switch语句存 在的理由。switch语句的基本格式是:switch (variable)case v
13、aluel : break;case value2 :break; default :break;每个case语句的结尾不要忘了加break,否则将导致多个分支重叠(除非 有意使多个分支重叠)。 不要忘记最后那个default分支。即使程序真的不需要default处理,也应 该保留语句default : break;这样做并非多此一举,而是为了防止别人误 以为你忘了 default处理。"l4循环结构c循环语句中,for语句使用频率最高,while语句其次,do语句很少用。 木节重点论述循环体的效率。提高循环体效率的基木办法是降低循环体的复杂 性。在多重循环中,如杲有可能,应当将最长
14、的循环放在最内层,最短的循环放 在最外层,以减少cpu跨切循环层的次数。例如示例1.4(b)的效率比示例1.4(a) 的高。for (row=0; row<100; row+)for ( col=0; col<5; col+ )sum = sum + a|row|col|;for (col=0; col<5; col+ )for (row=0; row<100; row+)sum = sum + arow|col|;示例1.4(a)低效率:长循环在最外层 示例1.4(b)高效率:长循环在授内层如果循坏体内存在逻辑判断,并口循环次数很大,宜将逻辑判断移到循坏体 的外面。示
15、例1.4(c)的程序比示例1.4(d)多执行了 n-1次逻辑判断。并且由于 前者老要进行逻辑判断,打断了循坏“流水线”作业,使得编译器不能对循坏进 行优化处理,降低了效率。如果n非常大,最好采用示例1.4(d)的写法,可以 提高效率。如果n非常小,两者效率差别并不明显,采用示例1.4(c)的写法比 较好,因为程序更加简洁。for (i=0; i<n; i+)if (condition)if (condition)for (i=0; i<n; i+)dosomethingo ;dosomethingo ;elsedootherthingo ;elsefor (i=0; i<n;
16、 i+)dootherthingo ;示例1.4(c)效率低但程序简洁 示例1.4(d)效率高但程序不简洁1.5常量常量是一种标识符,它的值在运行期间恒定不变。c语言用#define来定义常量 (称为宏常量)。除了 define夕卜还可以用const来定义常量(称为const常量)。1.5.1为什么需要常量如果不使用常量,直接在程序中填写数字或字符串,将会有什么麻烦?(1)程序的可读性(可理解性)变差。程序员门己会忘记那些数字或字符串是 什么意思,用户则更加不知它们从何处來、表示什么。(2)在程序的很多地方输入同样的数字或字符吊,难保不发生书写错误。(3)如果要修改数字或字符申,则会在很多地方
17、改动,既麻烦又容易出错。尽量使用含义直观的常量来表示那些将在程序中多次出现的数字例如:#define const int const float或字符串。max 100/* c语言的宏常量*/max 二 100;/ c+ 语言的 const 常量pi = 3. 14159; / c+ 语言的 const 常量1.5.2 const 与 #define 的比较可以用const來定义常量,也可以用ttdefine來定义常量。但是前者比后者 有更多的优点:(1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行 类型安全检杳。而对后者只进行字符替换,没有类型安全检查,并且在字 符替
18、换可能会产生意料不到的错谋(边际效应)。(2)有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进 行调试。1.5.3常量定义规则需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的 头部。为便于管理,可以把不同模块的常量集屮存放在一个公共的头文件屮。如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出 一些孤立的值。例如:const float radius = 100;const float diameter = radius * 2;1.5.4 ansic 中的 const问题1: const所定义的到底是变量还是常量为什么象下面的例子一样用一
19、个const变量来初始化数组,ansi c的编译 器会报告一个错误呢?const int n = 5;int an;答案与分析:1)、这个问题讨论的是“常量”与“只读变量'的区别。常量肯定是只读的, 例如5, “abc”,等,肯定是只读的,因为程序中根本没有地方存放它的值, 当然也就不能够去修改它。i只读变量”则是在内存屮开辟一个地方来存放 它的值,只不过这个值由编译器限定不允许被修改。c语言关键字const就 是用来限定一个变量不允许被改变的修饰符(qualifier)。上述代码屮变量n 被修饰为只读变量,可惜再怎么修饰也不是常量。而ansic规定数组定义 时维度必须是“常量”,“只
20、读变量”也是不可以的。2)、注意:在ansi c中,这种写法是错误的,因为数组的大小应该是 个常量,ifij const int n,n只是一个变量(常量!=不可变的变量,但在标准 c+中,这样定义的是一个常量,这种写法是对的),实际上,根据编译过 程及内存分配来看,这种用法本来就应该是合理的,只是ansic对数组的 规定限制了它。3) 、那么,在ansi c语言屮用什么来定义常量呢?答案是enum类型 和#define宏,这两个都可以用来定义常量。问题2: const所限定的内容下面的代码编译器会报一个错误,请问,哪一个语句是错误的呢? typedef char * pstr;char st
21、ring4 = ” abc”;const char *p1 = string;const pstr p2 = string;p1+;p2+;答案与分析:问题岀在p2+上。1) > const使用的基木形式:const char m;限定m不可变。2) > 替换 1 式中的 m, const char *pm;限定*pm不可变,当然pm是可变的,因此问题中p1+是对的。3) 、替换 1 式 char, const newtype m;限定m不可变,问题中的pstr就是一种新类型,因此问题中p2不 可变,p2+是错误的。问题3:字符串常量请问下而的代码有什么问题? char *p =
22、”i'm hungry!”; p0= t;答案与分析:上面的代码可能会造成内存的非法写操作。分析如下,hungry”实质 上是字符串常量,而常量往往被编译器放在只读的内存区,不可写。p初始 指向这个只读的内存区,而p0 = t则企图去写这个地方,编译器当然不会 答应。问题4:字符串常量2请问char a3 = uabcu合法吗?使用它有什么隐患? 答案与分析:在标准c中这是合法的,但是它的生存环境非常狭小;它定义一个大小 为3的数组,初始化为“abc”,注意,它没冇通常的字符串终止符0,因此 这个数组只是看起来像c语言中的字符串,实质上却不是,因此所有对字符 串进行处理的函数,如str
23、cpy、printf等,都不能够被使用在这个假字符串上。问题5: const &指针类型声明中const用來修饰一个常量,有如下两种写法,那么,请问, 下面分别用const限定不可变的内容是什么?1) 、const在前面const int nvalue; nvalue 是 constconst char *pcontent; /广pcontent 是 const, pcontent 可变 const (char *) pcontent;/pcontent 是 const,*pcontent 可变 char* const pcontent; /pcontent 是 con st,*pc
24、on tent 可变 const char* const pcontent; /pcontent 和*pcontent 都是 const2) 、const在后面,与上面的声明对等int const nvalue; / nvalue 是 constchar const * pcontent;/ *pcontent 是 const, pcontent 可变 (char *) const pcon tent;/pc on tent 是 const,*pcontent 口j 变 char* const pcontent;/ pcon tent 是 con st,*pc on tent 可变 char
25、const* const pcontent;/ pcontent 和*pcontent 都是 const 答案与分析:const和指针一起使用是c语言中一个很常见的困惑之处,在实际 开发中,特别是在看别人代码的时候,常常会因为这样而不好判断作者 的意图,下面讲一下我的判断原则:沿着*号划一条线,如果const位于的左侧,则const就是用來修饰 指针所指向的变量,即指针指向为常量如果const位于啲右侧,const 就是修饰指针木身,即指针木身是常量。你可以根据这个规则來看上面 声明的实际意义,相信定会一目了然。另外,需要注意:对于const (char *);因为char 是一个整体,ffl
26、 当于一个类型(如char),因此,这是限定指针是consto第2章函数函数是c程序的基本功能单元,其重要性不言而喻。函数设计的细微缺点很 容易导致该函数被错用,所以函数的功能止确是不够的。函数接口的两个要素是参数和返冋值。c语言中,函数的参数和返冋值的传 递方式有两种:值传递和地址传递。2.1参数的规则参数的书写要完整。不耍贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用 void填充。例如:void setvalue(int width, int height) ; / 良好的风格/不良的风格/良好的风格/不良的风格void sctvalue(int, int); float
27、getvalue(void); float getvalue ();参数命名要恰当,顺序要合理。例如编写字符串拷贝函数stringcopy,它冇两个参数。如果把参数名字起为 strl 和 str2,例如:void stringcopy(char *strl, char *str2);那么我们很难搞清楚究竞是把strl拷贝到str2屮,还是刚好倒过来。可以把参数名字起得更冇意义,如叫strsource和strdestinationo这样从 名字上就可以看岀应该把strsource拷贝到strdestinationo述冇一个问题,这两个参数那一个该在前那一个该在后?参数的顺序耍遵循 程序员的习惯。
28、一般地,应将目的参数放在前面,源参数放在后面。如果将函数声明为:void stringcopy(char *strsource, char *strdestination);别人在使用时可能会不假思索地写成如下形式:char str20;stringcopy(str, “hello world”);/ 参数顺序颠倒指针参数如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在 函数体内被意外修改。例如:void str in gcopy (char strdcsti neiti on, const char strsource);避免函数有太多的参数,参数个数尽量控制在5个以内
29、。如果参数太多,在使用时容易将参数类型或顺序搞错。尽量不要使用类型和数目不确定的参数。c标准库函数printf是采用不确定参数的典型代表,其原型为:int printf (const chat format , argument );这种风格的函数在编译时丧失了严格的类型安全检查。2.2返回值的规则不要省略返回值的类型。c语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会冇什 么好处,却容易被误解为void类型。函数名字与返回值类型在语义上不可冲突。违反这条规则的典型代表是c标准库函数getcharo 例如:char c;c = getchar ();if (c 二二 eof)按照g
30、etchar名字的意思,将变量c声明为char类型是很门然的事情。但 不幸的是getchar的确不是char类型,而是int类型,其原型如下:int getchar(void);由于c是char类型,取值范围是-128, 127,如果宏eof的值在char的 取值范围z外,那么if语句将总是失败,这种“危险”人们一般哪里料得到! 导致本例错误的责任并不在用户,是函数getchar误导了使用者。不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返冋。回顾上例,c标准库函数的设计者为什么要将getchar声明为int类型呢?在正常情况下,getchar的确返冋单
31、个字符。但如果getchar碰到文件结束 标志或发生读错误,它必须返冋一个标志eof。为了区别于正常的字符,只好将 eof定义为负数(通常为负1)。因此函数getchar就成了 int类型。我们在实际工作屮,经常会碰到上述令人为难的问题。为了避免出现误解, 我们应该将正常值和错误标志分开。b|j:正常值用输出参数获得,而错误标志用 return语句返冋。函数 getchar 可以改写成 bool getchar (char *c);虽然 gechar 比 getchar 灵活,例如 putchar (getchar ();但是如果 getchar 用错了,它的灵活性乂有什么用呢?有时候函数原本
32、不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。例如字符串拷贝函数strcpy的原型:char *strcpy (char *strdcst, const char *strsrc);strcpy函数将strsrc拷贝至输出参数strdcst中,同时函数的返回值又是 strdcsto这样做并非多此一举,可以获得如下灵活性:char str20;int length = strlen( strcpy(str, “hello world” );2.3函数内部实现的规则不同功能的函数其内部实现各不相同,看起來似乎无法就“内部实现”达成 一致的观点。但根据经验,我们可以在函数体的“入口处
33、”和“出口处”从严把 关,从而提高函数的质量。在函数体的“入口处”,对参数的有效性进行检查。很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言” (assert)来防止此类错误。详见2. 5节“使用断言”。在函数体的“出口处”,对return语句的正确性和效率进行检查。如果函数有返回值,那么函数的“出口处”是return语句。我们不要轻视 return语句。如果return语句写得不好,函数要么岀错,要么效率低下。注意事项如下: return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该 内存在函数体结束时被自动销毁。例2. 1char * func(void)char
34、 st讥二 “hello world" ;/ str 的内存位于栈上 return str;/将导致错误要搞清楚返回的究竞是“值”还是“指针”。2.4其它建议函数的功能要单一,不要设计多用途的函数。函数体的规模要小,尽量控制在50行代码之内。尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可 能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。 在c语言屮,函数的static局部变量是函数的“记忆”存储器。建议尽量少 用static局部变量,除非必需。不仅要检查输入参数的有效性,还要检查通过其它途径
35、进入函数体内的变量的有效性,例如全局变量、文件句柄等。用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。2.5使用断言程序一般分为debug版本和release版本,debug版本用丁内部调试,release 版本发行给用户使用。断言assert是仅在debug版本起作用的宏,它用于检查“不应该”发生的情 况。示例65是一个内存复制函数。在运行过程中,如果assert的参数为假,那 么程序就会中止(一般地还会出现捉示对话,说明在什么地方引发了 assert)。例 2.2:void *mcmcpy(void *pvto, const void *pvfrom, size t si
36、ze)assert (pvto != null) && (pvfrom != null) ;/ 使用断言byte *pbto = (byte *) pvto; / 防止改变 pvto 的地址 byte *pbfrom = (byte *) pvfrom; / 防止改变 pvfrom 的地址 while (size - > 0 )*pbto + = *pbfrom + ;return pvto;为了不在程序的debug版木和release版木引起羌另ij, assert不应该产生任何 副作用。所以assert不是函数,而是宏。程序员可以把assert看成一个在任何系 统状态
37、卜都可以妥全使用的无害测试手段。如果程序在assert处终止了,并不是 说含有该assert的函数有错误,而是调用者出了差错,assert可以帮助我们找到 发生错误的原因。如果搞不清楚断言检查的是什么,就很难判断错误是出现在程序中,还是出 现在断言中。幸运的是这个问题很好解决,只耍加上清晰的注释即可。这本是显 而易见的事情,可是很少有程序员这样做。这好比一个人在森林里,看到树上钉 着一块“危险”的大牌了。但危险到底是什么?树耍倒?有废井?有野曽?除非 告诉人们“危险”是什么,否则这个警告牌难以起到积极有效的作用。难以理解 的断言常常被程序员忽略,甚至被删除。在函数的入口处,使用断言检查参数的有
38、效性(合法性)。在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定? ”一 旦确定了的假定,就要使用断言对假定进行检査。第3章指针、数组和字符串3.1指针的本质程序是由指令和数据组成的,其中数据在程序运行时是存放在内存单元中 的,指针的本质就是这个内存单元的地址。首先,指针是变量,它和常用的整型 变量、字符变量等没有本质区别,不同的是整型变量的值是整型,字符变量的值 是字符类型,而指针变量的值是一个内存单元的地址,即通常是一个32位的二 进制数字(32位系统)。通过下面的程序,我们可以查看指针的真实面貌。例 3.1:#include <stdio.h>int main(
39、int argc, char *argv)int i = 15;char c = 'o'float f = i;int *pi = &i;int *ppi = πprintf("address of i is : ox%pn", &i);printf("value of pi is : ox%pn”, pi);printf(haddress of pi is : ox%pn”, &pi);printf("value of pi is : ox%pnn' ppi);printfc'val
40、ue of c is : %cn”, c); printf("address of c is : ox%pnn", &c);printfc'value of f is : %fn", f);printf("address of f is : ox%pnn", &f);getch();输fl!结果:address ofiis: 0x0012ff7cvalue of pi is : 0x0012ff7caddress of pi is : 0x0012ff70value of ppi is : 0x0012ff70value
41、 of c is : 0address of i is : 0x0012ff78value of f is :15.000000address of i is : 0x0012ff74观察输出结果,变量pi的值就是i变量的地址的值,变量ppi的值就是pi变 量的地址的值。通过这个例子可以了解到指针的本质,可以将指针变量看作一个 32位的二进制整数,而这个整数正是一个内存单元的地址,通过访问这个内存 地址口j以访问到该指针指向的内存单元的值。3.2指针的类型及其支持的运算指针的类型实际上指针指向的内存单元所存放的数据的类型。int *plnt;int *pplnt;char *pchar;voi
42、d *pvoid;pint的类型是int *,也就是整型指针。pplnt的类型是int*,即整型指针的 指针。pchar的类型是char*,即字符类型的指针。通常我们可以通过使用typedef把不同的指针类型定义下来,然后直接使用定义后的类型。 typedefint * typedefint* typedefchar* typedef void*intptr; intptrptr; charptr; voidptr;为什么要使用上面的定义呢,通过下面的例子我们可以看到它的作用。例 3.2:#include <stdio.h>typedef int* intptr;int main(
43、 int argc, char *argv)int i = 15;int* ipa, ipb;intptr pa, pb;ipa = &i;ipb = &i;pa = &i;pb = &i;getch();在该程序中设置断点单步跟踪ipa, ipb, pa, pb这四个变量,从跟踪结果中可以发现ipa是int*类型,而ipb仍然是int类型;而pa、pb都是int*类型。这 说明在语句:int*ipa,ipb;中,*符号结合到了 ipa中,实际上是等效于:int *ipa; int ipb;rfn使用intptr的方式来定义的两个变量等效于int* pa; in
44、t* pb;因此如果需 要将两个变量都定义为指针类型就需要使用intptr,在使用int*时需耍注意其中* 号的结合性。全局指针变量的默认初始值是null。而对于局部指针变量p,你必须显示 地指定其初值,否则p的初始值是不可预测的(不是null)o当你第一次使用 它的时候就可能会用if( p != null)来检查p的有效性,然而此时if语句的确不 起作用。如果忘记给p赋初值,那么你第一次使用p的时候就会倒是运行时错误。作为一个好的编程习惯,不管指针变量是全局还是局部的、静态的还是非静 态的,都应该要么为指针变量赋予一个有效的初始值,耍么将其初始化为null。从指针的本质來分析,它实际上是一个
45、整数,因此它应该可以进行整数能够 参与的所有算术运算,但是由于它的本质是内存地址,所以很多算术运算对指针 都没有意义。使用较多的运算如下:指针自增(+)指针自减()指针加n(n为正整数)指针减n(n为正整数) 指针比较(常用的是二二和!二)指针赋值取地址和反引用例 3.3:int a5();int* pa;int b=0;int* pb;pa = a; 指针赋值,a相当与a数组的地址,也可以看作指针pa+;这时pa指向a数组的第二个元素,实际上地址不是加一/而是加上了 sizeof(int)pa+=20;/这时pa指向a数组的第二十二个元素,指针加上了 20*sizeof(int) if( p
46、a !=null)指针比较b = *pa;反引用,*pa表示pa所指的内存单元的值pb = &b;取地址,pb取b变量的内存单元的地址需要注意void*类型的指针不能参与算术运算,只能进行赋值、比较和sizeof 操作,同样也不能对void*类型的指针使用*符号来反引用,因为其指针所指的值 类型无法确定因此无法对其取值。3.3指针传递如果函数的参数或返冋值被声明为指针类型,那么函数接受的参数或返冋值 就是地址,而不是指针所指的内存单元的值。例 3.4:#include <stdio.h>void funcptr( int *p)(*p)+;将指针所指向的内存单元的值加一p+
47、;将指针本身加一,即指向a数组的下一个元索int main( int argc, char * argv|)int a=1,2,3,4,5;int *pn;pa = a;printf("value of pa is :0x%pn", pa); printf(hvalue of a0 is :%dn”, a0); funcptr( pa);printf("value of pa is :0x%pn", pa); printf("value of a0 is :%dn”, a0); getch();输岀结果为:value of pa is: 0x(
48、)012ff6cvalue of a0 is: 1value of pa is: 0x0012ff6cvalue of a0 is: 2在执行完函数funcptr()后,a0的值壇加了一,而指针pa本身的内容并没冇 发生改变,也就是pa所指向的位置仍然是a0o这是为什么呢?我们知道函数 的参数传递分为传值方式和传地址方式,传值方式的参数本身在函数内的修改在 函数结束后不会被保存,而传地址方式参数的在函数内的修改在函数结束后被保 存下來了。这里的函数funcptr()实际上就是采用的传地址方式,因为pa所指的 值在函数结束后的确被修改了,但为什么pa指针本身的值的修改却没有被保存 呢。实际上从严
49、格的意义上來说函数的参数传递全部都是值传递,没有所谓的地 址传递,参数本身在函数中的修改都是不会被保存的,因为进入函数时,参数的 值都会被复制一个备份來在函数中使用,而不是直接使用参数,这样可以保证参 数的值在退出函数时不会发生变化。所谓的传地址方式,实际上是将指针作为参 数传递,那么参数实际上一个指针,也就是一个内存单元的地址(一个32位的 整数),而不是参数所指向的内存单元的值,因此在函数小指针本身是被保护的, 而指针所指向的内存单元的值并不是参数,它没有被作为参数来保护,所以上例 小指针所指向的内存单元的值被修改了,而指针本身并没有被修改。下面是一个关于内存屮请的函数的例了:例 3.5:
50、#include <stdio.h>void allocate( char *p, int size )中请size大小的内存空间,并将首地址赋值给pp = ( char * )malloc(size*sizeof(char);int main( int argc, char* argv)int *pa;pa = null;allocate( pa, 50);if( pa = null)printf(" value of pa is null !n”);elseprintf("value of pa is :0x%pn”,pa);getch();可能大家认为这个
51、程序是没有问题的,但是allocate函数执行后,pn的值确 为null。如果大家深刻理解了上一个例了,那么这个例了就容易分析了,原因 就是allocate函数中将char* p作为参数,主程序中将pa作为参数传入,那么函 数需要保护参数“并保证pa的值(是其木身的值,即地址木身,而不是指针指 向的内存单元的值)不被修改,而函数中对参数p做了赋值,而且这个赋值是直 接对指针木身的修改,因此这个修改不会被保存。很多时候我们都需要一个像allocate这样的函数来完成内存的中请,那怎么 样才能在函数中完成这一功能并将分配的内存的地址传递出函数呢?有两种方 法:第一种方法是将这个地址作为返冋值來传递
52、出函数。例 3.6:char* allocate 1( size )return( (char * )malloc( size * sizeof(char);allocate 1函数直接返回了中请的内存的首地址,如果在主程序屮使用pa来接 收返回值就可以得到申请的内存的首地址了。第二种方法是使用指针的指针作为参数,因为将指针的指针作为参数时,函数保护的是指针的指针,而不会保护指针本身了。例 3.7:#include <stdio.h>void allocate2( char int size )申请size大小的内存空间,并将首地址赋值给*p*p = ( char * )mallo
53、c(size*sizeof(char);int main( int argc, char* argv)int *pa;pa = null;取pa的地址,即指针的指针allocate2( &pa, 50);if(pa = null)printf(u value of pa is null !nu);elseprintf("value of pa is :0x%pn", pa);getch();allocate2函数将申请内存的地址传递给了*p,也就是pa,这样实现了内存首 地址的参数传递。最后需耍注意的是:我们可以将指针或指针的指针作为参数或变量进行传 递,但是在传递
54、的过程当屮要注意指针所指向的地址一定要是有效的,如果访问 的指针无效可能会使整个程序崩溃。如果使用指针接收malloc申请的内存的首 地址,一定要在程序结朿以前将申请的内存释放(通过free函数),不要在指针 的传递或运算过程中丢失了首地址的值,或是忘记了内存释放,这样都会造成内 存泄露,内存泄露会使你的程序出现问题,特别是在嵌入式设备上运行的程序, 其至会导致程序终止,这绝不是危言耸听。3.4数组的本质任何数组,其所有元素的内存小都是连续字节存放的,也就是说保存在i大 块连续的内存区屮。数组元素的下标编号从0开始,最后一个元素的下标等于元 素个数减-。下标必须是整数或整数的表达式。通过下标引
55、用一个数组元素,在 本质上和引用一个同类型的变量没什么区别,编译器通过下标值来计算你所引用 的元素在内存小的地址。因此,在语义上,下标操作符返回的是一个元素的引用。int a5 = 1,2, 3,4,5 ;int *pa = a;a3 = 100;假设数组a的地址为0x0012ff6c,如果需要计算a3,那么编译器会计算 a3的地址,即 a+3*sizeof(int) = 0x0012ff6c + 3*4 = oxoo12ff78,然后将 oxoo12ff78所指向的内存单元的值修改为100。当然,这个计算是由编译器来 完成的,我们只需耍理解它的完成方法就可以了。pa和a的值实际上是相同的,
56、pa指向的就是数组a的首地址,那么a3也可以表示为*(p+3),对于这里的 (p+3),编译器会白动将其转变为p + 3*sizeof(int),所以可以认为pa也是数组a 的别名,甚至可以这样来访问:a3和pa3是等效的,*(a + 3)和*(p+3)也是等 效的。也就是说数组名就是指针,但是a作为数组a的首地址,它是不能被修改 的,rfop是可以被修改的。a=a+l;p = p+1;上面第一个语句是错误的,a不能被修改,但是第二句是正确的。在声明数组时,一般有如下三种方式:明确地指出它的元素个数,编译器会按照给定的元素个数来分配存储空 间。例如:iiha10;不指明元素个数而直接初始化,编译器会根据你提供的初始值的个数来 确定数组的元素个数。例如:int a = 1,2, 3,4,5 ;同时指定元素个数并且初始化。例如:int a5 = 1,2, 3,4,5 ;但是不允许既不指定元素个数、乂不初始化,因为编译器不知道到底该为数 组分配多少存储空间。另外需要注意的是定义数组时,不
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2024版房产出售附带产权车位买卖合同3篇
- 2024版城乡实体地域划分与城乡信息化基础设施建设合同3篇
- 北京墓地销售合同范例
- 餐饮会员合同范例
- 陶瓷洁具购销合同模板
- 2024年度建筑工程环境保护合同
- 2024年度房产项目销售代理合同3篇
- 项目转让协议合同模板
- 山东政府采购合同范例
- 2024年度打印机设备智能化改造及远程监控服务合同3篇
- 福建百校2025届高三12月联考历史试卷(含答案解析)
- 2023年益阳市安化县招聘乡镇卫生院护理人员笔试真题
- 人音版音乐七年级上册《父亲的草原母亲的河》课件
- 2024年度短视频内容创作服务合同3篇
- 2024年度拼多多店铺托管经营合同2篇
- 2023年北京肿瘤医院(含社会人员)招聘笔试真题
- 能源管理总结报告
- 2024年时事政治试题库
- 2024-2025学年统编版五年级语文上册第七单元达标检测卷(原卷+答案)
- 【初中数学】基本平面图形单元测试 2024-2025学年北师大版数学七年级上册
- 旅行社分店加盟协议书(2篇)
评论
0/150
提交评论