<<C和指针>>看书笔记
int ch
while( (ch = getchar()) != EOF && ch != ‘\n’);
这个地方首先要注意的是赋值运算符的优先级比比较运算符的优先给低,所以ch = getchar()需要用括号括起来,另外这里的ch被定义为int型,因为getchar()的返回值为整型,另外,EOF为一个整型常量(在stdio.h中定义)
上面中的那个语句也可以按如下写:
ch = getchar();
While(ch != EOF && ch != ‘\n’)
ch = getchar();
char input[50];
while ( gets(input) != NULL);
上面这句话实际上是有问题的,因为gets()不会对input数组的溢出做检查,所以当输入一行的字符特别多时,就会出错.
三字母词,
下面例出的是一些三字母词,及其所代表的字符:
??( [ ??< { ??= #
??) ] ??> } ??/ \
??! | ??’ ^ ??- ~
如printf(“??(”);输出的结果为: [
字符在本质上是小整型值,缺省的char要么是signed char,要么是unsigned,这取决于编译器.这个事实意味着不同的机器上的char可能拥有不同范围的值,所以,只有当程序所使用的char型变量的值位于singned char和unsigned char的交集中,这个程序才是可移植的.例如ASCII字符集中的字符都是位于这个范围之内的.
在一个把字符当作小整型值的程序中,如果显式地把这类变量声明为signed或unsigned,可以提高这类程序的可移植性.另外,许多处理字符的库函数把它们掺数声明为char,如果你把参数显示声明为unsigned char 或signed char,可能会带来兼容性问题.
声明指针
int *a;
上面这条语句表示表达式*a产生的结果类型是int.知道了*操作符执行的是间接访问操作后,我们可以推断a肯定是一个指向int的指针.
Int* a;这个声明与前面一个声明具有相同的意思,而且看上去更为清楚,a被声明为类型为int*的指针,但是,这并不是一个好的技巧,原因如下:
int* b, c, d;
上式中,*号实际上是表达式*b的一部分,只对这个标识符有用,b是一个指针,但其余两个变量只是普通的整型.
char * message = “Hello World!”;
这种类型的声明所面临的一个危险是你容易误解它的意思,在这个声明中,看上去初始值似乎赋给表达式*message,事实上,它是赋给message本身的.换句话说,前面一个声明相当于:
Char *message;
message = “Hello World!”;
常量:
Int const a;
Const int a;
上面两条语句意思相同,都把a声明为一个整数,且它的值不能修改.
int *pi;
pi是一个普通的指向整型的指针.
int const *pci;
pci是一个指向整型常量的指针.你可以修改指针的值,但你不能修改它所指向的值.
int * const cpi;
cpi是一个指向整型的常量指针.此时,指针是常量,它的值无法修改,但你可以修改它所指向的整型值.
int const * const cpci;
在cpci这个例子里,无论是指针本身还是它所指向的值都是常量,不允许修改.
表达式语句
在C中,赋值就是一种操作,就像加减法一样,所以赋值就在表达式内进行.你只要在表达式后面加上一个分号,就可以把表达式转变为语句.所以下面这两个表达式实际上是表达式语句,而不是赋值语句.
x = y + 3;
ch = getchar();
理解这一点非常重要,因为像下面这样的语句也是完全合法的.
y + 3;
getchar();
当这些语句被执行时,表达式被求值,但它们的结果并不保存于任何地方,因为它并未使用赋值操作符.因此,第一条语句并不具备任何效果,而第二条语句则读取输入中的下一个字符,但接着便丢弃了.
Printf(“Hello World!\n”);
Printf()是一个函数,函数将会返回一个值(它实际所打印的字符数),但我们并不关心这个值,所以弃之不理也很正常.所谓语句”没有效果”只是表达式的值被忽略.printf()函数所执行的是有用工作,这类作用称为”副作用”.
还有一个例子:
a++;
这条语句并没有赋值操作符,但它却是一条非常合理的表达式语句.++操作符将增加变量a的值,这就是它的副作用.另外有一些具有副作用的操作符.
在switch语句中,continue语句没有任何效果.只有当switch语句位于某个循环内部时,你才可以把continue语句放在switch语句内.在这种情况下,与其说continue语句作用于switch语句,还不如说它作用于循环.
在switch…case语句中,如果特意不要break语句,那最好是在break那里加上一条注释,便于以后维护!
关于移位操作:
标准说明无符号值执行的所有移位操作都是逻辑移位,但对于有符号值,到底是采用逻辑移位还是算术移位取决于编译器.你可以编写一个简单的测试程序,看看你的编译器使用哪种移位方式.但你的测试并不能保证其它编译器也会使用同样的方式.因此,一程序如果使用了有符号数的右移位操作,它就是不可移植的.
注意类似这种形式的移位:
a << -5;
左移-5位表示什么呢?是表示右移5位吗?还是根本就不移位?在某台机器上,这个表达式实际执行左移27位的操作-----你怎么也想不出来吧!如果移位的位数比操作数的位数还要多,会发生什么情况呢?
标准说明这类移位的行为是未定义的,所以字是由编译器来决定的.然而,很少有编译器设计者会清楚的说明如果发生这种情况会是怎样,所以它的旨查很可能是没有意义的.因此,你应该避免使用这种类型的移位,因为它们的效果是不可预测的,使用这类移位的程序是不可移植的.
赋值:
赋值也是一个表达式,是表达式就具有一个值.赋值表达式的值就是左操作数据的新值,它可以作为其他赋值操作符的操作数.如:
r = s + ( t = u – v ) /3;
再来看以前例出的一个列子:
char ch;
While ( (ch = getchar() ) != EOF)……
EOF为一个整型常量值,它需要的位数比字符值所能提供的位数要多,这也是getchar()返回一个整型值而不是字符值的原因.然而,这里把getchar()的返回值首先存于字符型ch中将导致它被截短.然后这个被截短的值被提升为整型并与EOF比较.当这段存在错误的代码在使用有符号字符集的机器上运行时,如果读取了一个值为\377的字节时,循环将会终止,国为这个值截短再提升之后与EOF相等.当这段代码在使用无符号的字符集的机器上运行是,这个循环将永远不会终止!
复合赋值操作符
a += expression;
它的功能相当于下面的表达式:
a = a + (expression);
唯一的不同之处是+=操作符的左操作数(此例为a)只求值一次.注意括号:它们确保表达式在执行加运算之前已被完整求值,即使它内部包含有优先级低于加法运算操作符.
下面两条语句,如果函数f没有副作用,那么它们是等价的:
a[2 * (y – 6*f(x) )] = a[2 * (y – 6*f(x) )] + 1;
a[2 * (y – 6*f(x) )] += 1;
在第一种形式中,用于选择增值位置的表达式必须书写两次,一次在赋值号的左边,另一次在赋值号的右边.由于编译器无从知道函数f是否具有副作用,所以它必须两次计算下标表达式的值.第二种形式效率更高,因为下标只计算一次.
sizeof操作符
sizeof不是函数而是一个操作符,可以按如下使用:
sizeof(int); sizeof x; //后面可以有括号,也可以没有.
Sizeof(a = b + 1);这里是求表达式的长度,但并没有对表达式进行求值,所以这里并没有对a进行赋值.
++\--操作符:
以++为例,前缀形式的++操作符出现在操作数的前面:操作数的值被增加,而表达式的值就是操作数增加后的值. 后缀形式的++操作符出现在操作数的后面:操作数的值仍被增加,但表达式的值是操作数增加前的值.
int a, b, c, d;
a = b = 10; // a和b得到值10
c = ++a; //a增加至11,c得到的值为11
d = b++; //b增加至11, d得到的值为10
上面的注释描述了这些操作符的结果,但并不说明这些结果是如何获得的.抽像的说,前缀和后缀形式的增值操作符都复制一份变量的拷贝.用于周围表达式的值正是这份拷贝(在上面的例子中,”周围表达式”是指赋值操作). 前缀操作符在进行复制之前增加变量的值,后缀操作符在进行复制之后才增加变理的值.这些操作符的结果不是被它们所修改的变量,而是变量值的一份拷贝,认识这一点非常重要.它之所以重要是因为它解释了你为什么不能像下面这样使用这些操作符:
++a = 10;
++a这个表达式的结果是a值的一份拷贝,并不是变量本身,你无法对一个值进行赋值.
优先级和求值顺序
两个相邻操作符的执行顺序邮它们的优先级决定,如果它们的优先级相同,它们的执行顺序邮它们的结合性决定.除此之外,编译器可以自由决定使用任何顺序表达式进行求值,只要它不违背逗号,&&,||和?:操作符所施加的限制.
换句话说,表达式中操作符的优先级只决定表达式的各个组成部分在求值过程中如何进行聚组.这里有一个例子:
a + b*c
在这个表达式中,乘法和加法操作符是两个相邻的操作符.由于*操作符的优先级比+操作符高,所以乘法运算先于加法运算执行.编译器在这里别无选择,它必须先执行乘法运算.
下面是一个更有趣的表达式:
a*b + c*d + e*f
如果仅由优先级决定这个表达式的求值顺序,那么所有3个乘法运算将在所有加法运算之前进行.事实上,这个顺序并不是必需的.实际上,只要保证每个乘法运算在它相邻的加法运算之前执行即可.例如,这个表达式可以会以下面的顺序进行,其中粗体的操作符表示在每个步骤中进行操作的操作符:
a * b //乘号
c * d //乘号
(a*b) + (c*d) //加号
e * f //乘号
(a*b) + (c*d) + (e*f) //第二个加号
注意第一个加法运算在最后一个乘法运算之前执行.如果这个表达式按以下的顺序执行,其结果是一样的:
a * b //乘号
c * d //乘号
e * f //乘号
(a*b) + (c*d) //加号
(a*b) + (c*d) + (e*f) //第二个加号
加法运算的结合性要求两个加法运算按照先左后右的顺序执行,但它对表达式剩余的部分的执行顺序号并未加以限制.尤其是,这里并没有任何规则要求所有的乘法运算首先执行,也没有规则规定这几个乘法运算之间谁先执行.优先级规则在这里起不到作用,优先级只对相邻操作符的执行顺序起作用.
由于表达式的求值顺序并非完全由操作符的优先级决定,所以像下面这样的语句是很危险的:
c + --c
操作符的优先级规则要求自减运算在加法运算这前进行,但我们并没有办法得知加法操作符的左操作数是在右操作数之前还是在这后进行求值,它在这个表达式中将存在区别,因数自减操作符具有副作用.—c在c之前或这后执行,表达式的结果在两种情况下将会不同.
标准说明类似这种表达式的值是未定义的.尽管每种编译器都会为这个表达式产生某个值,但到底哪个是正确的并无标准答案.因此,像这样的表达式是不可移植的,应予以避免.
下面这个表达式:
f() + g() + h()
尽管左边那个加未能运算必须在右边那个加法运算之前执行,但对于各个函数调用的顺序,并没有规则加以限制.如果它们的执行具有副作用,比如执行一些I/O任务或修改全剧变量,那么函数调用顺序的不同可能会产生不同的结果.因此,如果顺序会导致结果产生区别,你最好使用临时变量,让每个函数调用都在单独的语句中执行:
temp = f();
temp += g();
temp += h();
*cp++的执行步聚如下:
(1)++操作符产生cp的一份拷贝
(2)然后++操作符增加cp的值
(3)最后,在cp的拷贝上执行间接访问操作
(注意这里++的优先级比*的高)
指针运算:
int *p, *p1;
p+2 实际上为: p + sizeof(*p)*2
p-2 实际上为: p – sezeof(*p)*2
p2 – p1 实际上为: (p1 – p)/(sizeof(*p1)), 两个指针相减时会执行除法操作,影响效率.
标准允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针进行比较,但不允许与指向数组第1个元素之前的那个内存位置的位指针进行比较.
如:
#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[0]; vp < &values[N_VALUES];)
*vp++ = 0;
for(vp= &values[N_VALUES-1]; vp>=&values[0]; vp--)
*vp = 0;
由标准可以,第二个for语句是有问题的.但实际上,在绝大多数C编译器中,这个循环将顺利完成任务.然而,你还是应该避免使用它,因为标准并不保语它可行.你迟早可能遇到一台这个循环失败的机器.对于负责可移植代码的程序员而言,这类问题简直就是个恶梦.
数组:
int a;
int b[10];
b[4]的类型是整型,但b的类型又是什么呢?它所表示的又是什么呢?一个合乎逻辑的答案是它表示整个数组,但事实并非如此.在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第1个元素的地址.它的类型取决于数组元素的类型:如果它们是int类型,那么数组名的类型就是”指向int的常量指针”;如果它们是其它类型,那么数组名就是”指向其它类型的常量指针”.
只有在两种场合下,数组名并不用指针常量来表示------就是当数组名作为sizeof操作符或单目操作符的操作数时.sizeof返回整个数组的长度,而不是指向数组的指针的长度,取一个数组名的地址所产生的是一个指向数组的指针!
数组和指针
指针和数组并不是相等的,为了说明这个概念,请考虑下面这两个声明:
int a[5];
int b;
a和b能够互换使用吗?它们都具有指针值,它们都可以进行间接访问和下标引用操作.但是它们还是存在相当大的区别.
声明一个数组时,编译器将根据声明所指定的元素数量为数组保留内存空间,然后再创建数组名,它的值是一个常量,指向这段空间的起始位置.声明一个指针变量时,编译器只为指针本身保留内存空间,它并不为任何整型值分配内存空间.而且,指针变量并未被初始化为指向任何现有的内存空间,如果它是一个自动变量,它甚至根本不会被初始化.因此,上述声明之后,表达式*a是完全合法的,但表达式*b却是非法的.*b将访问内存中某个不确定的位置,或者导致程序终止.另一方面,表达式b++可以通过编译,但a++却不行,因为a的值是一个常量.
字符数组的初始化
char message[] = { ‘H’, ‘e’, ‘l’, ‘l’, ‘o’, 0};
这个方法当然可行.但除了非常短的字符串,这种方法确实很笨拙.因此,语言标准提供了一种快递方法用于初始化字符数组:
char message[] = “Hello”;
尽管它看上去像是一个字符串常量,实际上并不是.它只是前例的初始化例表的另一种写法.
如果它们看上去完全相同,你如何分辨字符串常量和这种初始化例快递记法呢?它们是根据它们所处的上下文环境进行区分的.当用于初始化一个字符数组时,它已经一个初始化例表.在其他任何地方,它都表示一个字符串常量.
这里有一个例子:
char message[] = “Hello”;
char *message = “Hello”;
这两个初始化看上去很像,但它们具有不同的含义.前者初始化一个字符数组的元素,而后者则是一个真正的字符串常量.这个指针变量被初始化为指向这个字符串常量的存储位置.
多维数组:
考虑下列这些维数不断增加的声明:
int a;
int b[10];
int c[6][10];
int d[3][6][10];
a是个简单的整数.接下来的那个声明增加了一个维数,所以b就是一个向量,它包含10个整型元素.
c只是在b的基础上再增加了一维,所以我们可以把c看作是一个包含6个元素的向量,只不过它的每个元素本身是一个包含10个整型元素的向量.换句话说,c是一个一维数组的一维数组.d也是一如此:它是包含3个元素的数组.简洁的说,d是一个3排6行10列的整型三维数组.
数组名
一维数组名的值是一个指针常量,它的类型是”指向元素类型的指针”,它指向数组的第1个元素.多维数组也差不多简单.唯一的区别是多维数组第1维元素实际上是另一个数组.例如,下面这个声明:
int matrix[3][10];
matrix这个名字的值是一个指向它第1个元素的指针,所以matrix是一个指向一个包含10个整型元素的数组的指针.
指向数组的指针:
下面这些声明合法吗?
int vector[10], *vp = vector;
int matrix[3][10], *mp = matrix;
第一个声明是合法的.它为一个整型数组分配内存,并把vp声明为一个指向整型的指针,并把它初始化为指向vector数组的第1个元素.vector和vp具有相同的类型:指向整型的指针.但是第二个声明是非法的.它正确地创建了matrix数组,并把mp声明为一个指向整型的指针.但是mp的初始化是不正确的,因为matrix并不是一个指向整型的指针,而是一个指向整型数组的指针.我们应该怎样声明一个指向整型数组的指针的呢?
int (*p)[10];
这个声明比我们以前见过的所有声明更为复杂,但它事实上并不是很难.你只要假定它是一个表达式并对它进行求值.下标引用的优先级高于间接访问,但由于括号的存在,首先执行的还是间接访问,所以,p是一个指针,但它指向什么呢?接下来扫行的是下标的引用,所以p指向某种类型的数组.这个声明表达式中并没有更多的操作符,所以数组的每个元素都是整数.
声明并没有直接告诉你p是什么,但推断它的类型并不困难-----当我们对它执行间接访问操作时,我们得到的是个数组,对该数组进行下标引用操作得到的是一个整型值.所以p是一个指向整型数组的指针.
在声明中加上初始化后是下面这个样子:
int (*p)[10] = matrix;
它使p指向matrix的和1行.
p是一个指向拥有10个整型元素的数组的指针.当你把p与一个整数相加时,该整数值首先根据10个整型值的长度进行调整,然后再执行加法.所以我们可以使用这个指针一行一行地在matrix中移动.
如果你需要一个指针逐个访问整型元素而不是逐行在数组中移动,你应该怎么办呢?下面两个声明都创建了一个简单的整型指针,并以两种不同的方式进行初始化,指向matrix的第1个整型元素.
int *pi = &matrix[0][0];
int *pi = matrix[0];
增加这个指针的值使它指向下一个整型元素.
指针数组
int *api[10];
为了弄清楚这个复杂的声明,我们假定它是一个表达式,并对它进行求值.
下标引用的优先级高于间接访问,所以在这个表达式中,首先执行下标引用.因此,api是某种类型的数组(噢! 顺便说一下,它包含的元素个数为10).在取得一个数组元素之后,随即执行的是间接访问操作.这个表达式不再有其它操作符,所以它的结果是一个整型值.对数组的某个元素执行间接访问操作后,我们得到一个整型值,所以api肯定是个数组,它的元素类型是指向整型的指针.
高级指针话题
int *f();
要想推断出它的含义,你必须确定表达式*f()是如何进行求值的.首先执行的是函数调用操作符(),因为它的优先级高于间接访问操作符.因此,f是一个函数,它的返回值类型是一个指向整型的指针.
接下来的一个声明更为有趣:
int (*f)();
确定括号的含义是分析这个声明的一个重要步骤.这个声明有两对括号,每对的含义各不相同.第2对括号是函数调用操作符,但第1对括号只起到聚组的作用.它迫使间接访问在函数调用之前进行,使f成为一个函数指针,它所指向的函数返回一个整型值.
现在下面这个声明应该是比较容易弄懂了:
int *(*f)();
它和前一个声明基本相同,f也是一个函数指针,只是的所指向的函数的返回值是一个整型指针,必须对其进行间接访问操作才能得到一个整型值.
int (*f[])();
对于这个声明,首先你必须找到所有的操作符,然后按照正确的次序执行它们.同样,这里有两对括号,它们分别具有不同的含义.括号内的表达式*f[]首先进行求值,所以f是一个元素为某种类型的指针的数组.表达式末尾的()是函数调用操作符,所以f肯定是一个数组,数组元素的类型是函数指针,它所指向的函数的返回值是一个整型值.
如果搞清楚了上面最后一个声明,下面这个应该是比较容易的了:
int *(*f[])();
它和上面那个声明的唯一区别就是多了一个间接访问操作符,所以这个声明创建了一个指针数组,指针所指向的类型是返回值为整型指针的函数.
字符串常量:
当一个字符串常量出现于表达式中时,它的值是个指针常量.编译器把这些指定字符的一份拷贝存储在内存的某个位置,并存储一个指向第一个字符的指针.但是,当数组名用于表达式中时,它们的值也是指针常量.
下面这个表达式是什么意思呢?
“xyz” + 1;
对于绝大多数程序员而言,它看上去像堆垃圾.它好像是试图在一个字符串上面执行某种类型的加法运算.但是,当你记得字符串常量实际上是指针时,它的意义就变得清楚了.这个表达式计算”指针值加上1”的值.它的结果是个指针,指向字符串中的第2个字符:y.
那么这个表达式又是什么呢?
*”xyz”;
对一个指针执行间接访问操作时,其结果是指针所指向的内空.字符串常量的类型是”指向字符的指针”,所以这个间接访问的结果就是它所指向的字符:x,注意表达式的结果并不是整个字符串,而只是它的第1个字符.
下一个例子看上去也是有点奇怪,不过现在你应该对够推断出这个表达式的值就是字符z.
“xyz”[2];
下面这个语句是把二进制转换为字符:
putchar(“0123456789ABCDEF”[value%16]);
字符串,字符,字节
库函数strlen的原型:
size_t strlen(char const *string);
注意strlen返回一个类型为size_t(无符号整型),下面两个表达式看上去是相等的:
if(strlen(x) >= strlen(y))…..
if(strlen(x) – strlen(y) >= 0)….
但事实上它们是不相等的.第1条语句将接照你预想的那样工作,但第2条语句的结果将永远是真的.
字符串查找基础
在一个字符串中查找一个特定的字符最容易的方法是使用strchr和strrchr函数,它们的原型如下所示:
char *strchr(char const *str, int ch);
char *strchr(char const *str, int ch);
注意它们的第2个参数是一个整型值,但是它包括了一个字符值.strchr在字符串str中查找字符ch第1次出现的位置,找到后函数返回一个指向该位置的指针.如果该字符并不存在于字符串中,函数就返回一个NULL指针.strrchr的功能和strchr基本一致,只是它所返回的是一个指向字符串中该字符最后一次出现的位置(最右边那个).
查找任何几个字符
strpbrk是个更为常见的函数.它并不是查找某个特定的字符,而是查找任何一组字符第1次在字符串中出现的位置.它原型如下:
char *strpbrk(char const *str, char const *group);
这个函数返回一个指向str中第1个匹配group中任何一个字符的字符位置.如果未找到匹配,函数返回一个NULL指针.
在下面的代码段中:
char string[20] = “hello there, honey.”
char *ans;
ans = strpbrk(string, “aeiou”);
ans所指向的位置是string+1,因为这个位置是第2个参数中的字符e第一次出现的位置.这个函数是区分大小写的.
高级字符串查找
查找一个字符串前缀:
strspn和strcspn函数用于在字符串的起始位置对字符计数.它们的原型如下所示:
size_t strspn(char const *str, char const *group);
size_t strcspn(char const *str, char const *group);
group字符串指定一个或多个字符串.strspn返回str起始部分(从第一个字符开始)匹配group中任意字符的字符数.例如,如果group包含了空格,制表符等空白符,那么,这个函数将返回str起始部分空白字符的数目.str的下一个字符就是它的第一个非空白字符.
考虑下面这个例子:
int len1, len2;
char buffer[]=”25,142,330,Smith,J,239-4123”;
len1 = strspn(buffer, “0123456789”);
len2 = strspn(buffer, “,123456789”);
len1的值为2,从第一个字符开始,只有25可以匹配,再遇到”,”号时就结束匹配了.
len2的值为11,从第一个字符开始,匹配的内容为:”25,142,330,”当遇到字符S时匹配结束,因为字符S并不是第二个参数中的字符.
strcspn函数和strspn函数的功能正好相反,它对str字符串起始部分中不与group中任何字符匹配的字符进行计数.strcspn这个名字中字母c来源于对一组字符求补这个概念,也就是把这些字符换成原先并不存在的字符.如果你使用”\n\r\f\t\v”作为group参数,这个函数将返回第一个参数字符串起始部分所有非空白字符的值.
分隔字符串
一个字符串常常包含几个单独的部分,它们彼此被分隔开来.每次为了处理这些部分,你首先必须把它们从字符中抽取出来.
这个任务正是strtok函数所实现的功能.它从字符串中分离出被分隔符(由第二个参数字义)隔离的部分,并丢弃分隔符.它的原型如下:
char *strtok(char *str, char const *sep);
sep参数是个字符串,定义了用作分隔符的字符集合.第1个参数指定一个字符串,它包含零个或多个由sep字符串中一个或多个分隔符分离隔的子串.strok找到str的下一个子串,并将其它NULL结尾,然后返回一个指向这个子串的指针.
警告:
当strtok函数执行任务时,它将会修改它所处理的字符串.如果源字符串不能被修改,那就复制一份,将这份拷贝传递给strtok函数.
如果strtok函数的第一个参数不是NULL,函数将找到字符串的第一个字串(由分隔符分隔出来的子串).strtok同时将保存它在字符串中的位置.如果strtok函数的第一个参数是NULL,函数就在同一个字符串中从这个被保存的位置开始像前面一样查找下一个子串,如果字符串内不存在更多的子串,strtok函数就返回一个NULL指针.在典型情况下,在第一次调用相等的strtok时,向它传递一个指向字符串的指针.然后这个函数被重复调用(第一个参数置为NULL),直到它返回NULL为止.
下面是一个简短的例子.这个函数从第一个参数字符串中分离出由空白符分隔的子串,并把它们打印出来.
#include<stdio.h>
#include<string.h>
void print_tok(char *line)
{
static char whitespace[] = “\t\f\r\v\n”
char *token;
for(token = strtok(line, whitespace); token != NULL; token = strtok(NULL, whitespace) )
printf(“Next token is %s\n”, token);
}
如果你愿意,你可以在每次调用strtok函数时使用不同的分隔符集合,当一个字符串的不同部分由不同的字符集合分隔的时候,这个技巧很管用.
提示: 由于strtok函数保存它所处理的函数的局部状态信息,所以你不能用它同时解析两个字符串.因此,如果for循环的循环体内调用了一个在内部调用strtok函数的函数,则上面的例子将会失败.
位段
位段的声明和任何普通的结构体成员的声明相同,但有两处例外,首先,位段成员必须声明为int,signed int,unsigned int类型.其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位数目.
用signed或unsigned整数显式地声明位段是个好注意.如果把位段声明为int类型,它究竟被解释为有符号数还是无符号数是由编译器决定的.
注重可移植性的程序应该避免使用位段.由于下面这些与实现有关的依赖性,位段在不同的系统中可能有不同的结果.
1. int位段被当作有符号还是无符号数.
2. 位段中位的最大数目.许多编译器把位段成员的长度限制在一个整型值的长度之内,所以一个能够运行于32位整数的机器上的位段声明可能在16位整数的机器上无法运行.
3. 位段中的成员在内存中是从左向右分配还是从右向左分配的.
4. 当一个声明指定了两个位段,第2个位段比较大,无法容纳于第1个位段剩余的位时,编译器有可能把第2个位段放在内存的下一个字,也可能直接放在第1个位段后面,从而在两个内存位置的边界上形成重叠.
下面是一个位段声明的例子:
struct CHAR {
unsigned ch : 7;
unsigned font : 6;
unsigned size : 19;
};
struct CHAR ch1;
动态内存分配
下面这些函数在stdlib.h中声明
viod *malloc(size_t size);
void *calloc(size_t num_elements, size_t element_size);
void realloc(void *ptr, size_t new_size);
void free(void *pointer);
malloc的参数就是需要分配的内存字节数,malloc分配的是一块连续的内存,如果内存池中的可用内存可以满足这个需求,malloc就返回一个指向被分配的内存块起始位置的指针.如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针.因此,对每个从malloc返回的指针都进行检查,确保它并非NULL是非常重要的.
对于要求边界对齐的机器,malloc所返回的内存的起始位置将始终能够满足对边界对齐要求最严格的类型的要求.
mllloc和calloc之间的主要区别是后者在返回指向内存的指针之前把它初始化为0.这个初始化常常能带来方便,但如果你的程序只是想把一些值存储到数组中,那么这个初始化过程纯属浪费时间.
realloc函数用于修改一个原先已经分配的内存块的大小.使用这个函数,你可以使一块内存扩大或缩小.如果它用于扩大一个内存,那么这块内原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化.如果它用于缩小一个内存块,该内存块尾部的部分内存便被拿掉,剩余部分内存的原先内容依然保留.如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上.因此,在使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用realloc所返回的新指针.如果realloc函数的第一个参数是NULL,那么它的行为就和malloc一模一样.
free的参数是一个先前从malloc,calloc,realloc返回的指针.向free传递一个NULL参数不会产生任何效果.

被折叠的 条评论
为什么被折叠?



