wordpress 支付 api接口,网站的优化策略方案,wordpress 获取评论,那个网站专门做二手衣服的指针
地址和指针的概念
要明白什么是指针#xff0c;必须先要弄清楚数据在内存中是如何存储的#xff0c;又是如何被读取的。如果在程序中定义了一个变量#xff0c;在对程序进行编译时#xff0c;系统就会为这个变量分配内存单元。编译系统根据程序中定义的变量类型分配…指针
地址和指针的概念
要明白什么是指针必须先要弄清楚数据在内存中是如何存储的又是如何被读取的。如果在程序中定义了一个变量在对程序进行编译时系统就会为这个变量分配内存单元。编译系统根据程序中定义的变量类型分配一定长度的空间。内存的基本单元是字节一字节有8位。每字节都有一个编号这个编号就是“地址”它相当于旅馆的房间号。在地址所标示的内存单元中存放的数据就相当于在该旅馆房间中居住的旅客。
还有一种间接访问的方式即变量中存放的是另一个变量的地址。也就是说变量中存放的不是数据而是数据的地址。就跟寻宝一样可能你按藏宝图千辛万苦找到的宝藏不是金银珠宝而是另一张藏宝图。按C语言的规定可以在程序中定义整型变量、实型变量、字符型变量也可以定义这样一种特殊的变量它是存放地址的。
由于通过地址能找到所需的变量单元所以我们可以说地址“指向”该变量单元。如同一个房间号指向某一个房间一样只要告诉房间号就能找到房间的位置。因此在C语言中将地址形象地称为“指针”意思就是通过它能找到以它为地址的内存单元。
所以一个变量的地址就称为该变量的指针。指针就是地址而地址就是内存单元的编号。它是一个从零开始的、操作受限的非负整数。为什么是操作受限的因为非负整数与非负整数可以加减乘除但是指针和指针只能进行相减运算不能进行其他运算因为没有意义。而且进行相减运算也是有条件的只有同一块空间中的地址才能相减。而且两个指针变量相减之后的结果是一个常量而不是指针型变量即相减的结果是这两个地址之间元素的个数而不是地址的个数。 如果有一个变量专门用来存放另一个变量的地址那么就称它为“指针变量”。也就是说指针变量里面存放的是指针即地址。大家一定要区分“指针”和“指针变量”这两个概念。指针是一个地址而指针变量是存放地址的变量。
习惯上我们也将“指针变量”简称为“指针”但大家心里一定要明白这两个指针的区别。一个是真正的指针它的本质是地址而另一个是指针变量的简称。
指针和指针变量
为了表示指针变量和它所指向的变量之间的联系在程序中用“*”表示“指向”。如果定义变量i为指针变量那么*i就表示指针变量i里面存放的地址所指向的存储单元里面的数据。很绕吧好好体会一下。
指针变量的定义
指针变量定义的一般形式为 基类型 *指针变量名;
比如 int *i;float *j;
说明
1“*”表示该变量的类型为指针类型。指针变量名为i和j而不是*i和*j。
2在定义指针变量时必须指定其基类型。指针变量的“基类型”用来指定该指针变量可以指向的变量的类型。比如“int *i; ”表示i只可以指向int型变量又比如“float *j; ”表示j只可以指向float型变量。
3为什么叫基类型而不直接叫类型因为比如“int *i; ”其中i是变量名i变量的数据类型是“int *”型即存放int变量地址的类型。“int”和“*”加起来才是变量i的类型所以int称为基类型。
4“int *i; ”表示定义了一个指针变量i它可以指向int型变量的地址。但此时并没有给它初始化即此时这个指针变量并未指向任何一个变量。此时的“*”只表示该变量是一个指针变量至于具体指向哪一个变量要在程序中指定。这个就跟定义了“int j; ”但并未给它赋初值一样。
5因为不同类型的数据在内存中所占的字节数是不同的比如int型数据占4字节char型数据占1字节。而每个字节都有一个地址比如一个int型数据占4字节就有4个地址。那么指针变量所指向的是这4个地址中的哪个地址呢指向的是第一个地址即指针变量里面保存的是它所指向的变量的第一个字节的地址即首地址。因为通过所指向变量的首地址和该变量的类型就能知道该变量的所有信息。
6指针变量也是变量是变量就有地址所以指针变量本身也是有地址的。只要定义了一个变量程序在运行时系统就会为它分配内存空间。但指针变量又是存放地址的变量所以这里有两个地址大家一定要弄清楚一个是系统为指针变量分配的地址即指针变量本身的地址另一个是指针变量里面存放的另一个变量的地址。这两个地址一个是“指针变量的地址”另一个是“指针变量的内容”。
7地址也是可以进行运算的我们后面会学到指针的运算和移动。比如“使指针向后移1个位置”或“使指针加1”这个1代表什么呢这个1与指针变量的基类型是直接相关的。指针变量的基类型占几字节这个1代表的就是几。比如指针变量指向一个int型变量那么“使指针移动1个位置”就意味着移动4字节“使指针加1”就意味着使地址加4。所以必须指定指针变量所指向的变量的类型即指针变量的基类型。某种基类型的指针变量只能存放该种基类型变量的地址。
8我们前面讲过两个指针变量相减的结果是一个常量而不是指针型变量。如两个“int *”型的指针变量相减结果是int型常量。此时要是把相减的结果赋给“int *”型就会报错。而且两个指针变量相减的结果是这两个地址之间元素的个数而不是地址的个数。原因也是一样的。比如两个“int *”型的指针变量相减第一个指针变量里面存放的地址是1245036第二个指针变量里面存放的地址是1245032那么这两个地址相减的结果是几是1而不是4。因为int型变量占4字节所以一个int元素就占4字节两个地址之间相差4个地址正好是一个int元素所以结果就是1。
指针变量的初始化
可以用赋值语句使一个指针变量得到另一个变量的地址从而使它指向该变量。比如 int i, *j;j i;
此外有两点需要注意
第一j不是i, i也不是j。修改j的值不会影响i的值修改i的值也不会影响j的值。j是变量i的地址而i是变量i里面的数据。一个是“内存单元的地址”另一个是“内存单元的内容”大家一定要分清楚好好理解一下。
第二定义指针变量时的“*j”和程序中用到的“*j”含义不同。定义指针变量时的“*j”只是一个声明此时的“*”仅表示该变量是一个指针变量并没有其他含义。而且此时该指针变量并未指向任何一个变量至于具体指向哪个变量要在程序中指定即给指针变量初始化。而当指定j指向变量i之后*j就完全等同于i了可以相互替换。
注意切忌将一个没有初始化的指针变量赋给另一个指针变量。这是非常严重的语法错误。
指针常见错误
1引用未初始化的指针变量
如果指针变量未初始化那么编译器会让它指向一个固定的、不用的地址。
在VC 6.0中只要指针变量未初始化那么编译器就让它指向0XCCCCCCCC这个内存单元。而且这个内存单元是程序所不能访问的访问就会触发异常所以也不怕往里面写东西。而如果在VS 2008这个编译器中程序虽然能编译通过但是在运行的时候直接出错它并不会像VC 6.0那样还能输出所指向的内存单元的地址。
下面来看一个程序
/*这个程序是错的引用未初始化的指针变量
*/
#includestdio.h
int main(void) {int i 3, j;*j i;return 0;
}
程序中j是int *型的指针变量。j中存放的应该是内存空间的地址然后“变量i赋给*j”表示将变量i中的值放到该地址所指向的内存空间中。但是现在j中并没有存放一个地址程序中并没有给它初始化那么它指向的就是0XCCCCCCCC这个内存单元。这个内存单元是不允许访问的即不允许往里面写数据。而把i赋给*j就是试图往这个内存空间中写数据程序执行时就会出错。但这种错误在编译的时候并不会报错只有在执行的时候才会出错即传说中的“段错误”。所以一定要确保指针变量在引用之前已经被初始化为指向有效的地址。
2往一个存放NULL地址的指针变量里面写入数据
我们把前面的程序改一下
/*这个程序是错的往一个存放NULL地址的指针变量里面写入数据
*/
#includestdio.hint main(void) {int i 3;int *j NULL;*j i;return 0;
}
之前是没有给指针变量j初始化现在初始化了但是将它初始化为指向NULL。NULL也是一个指针变量。NULL指向的是内存中地址为0的内存空间。以32位操作系统为例内存单元地址的范围为0x000000000xffff ffff。其中0x00000000就是NULL所指向的内存单元的地址。但是在操作系统中该内存单元是不可用的。凡是试图往该内存单元中写入数据的操作都会被视为非法操作从而导致程序错误。同样这种错误在编译的时候也不会报错只有在执行的时候才会出错。这种错误也属于“段错误”。
然而虽然这么写是错误的但是将一个指针变量初始化为指向NULL这在实际编程中是经常使用的。就跟前面讲普通变量在定义时给它初始化为0一样指针变量如果在定义时不知道指向哪里就将其初始化为指向NULL。只是此时要注意的是在该指针变量指向有效地址之前不要往该地址中写入数据。
最后关于NULL再补充一点NULL是定义在stdio.h头文件中的符号常量它表示的值是0。
指针作为函数参数
互换两个数怎么实现先写个程序看看
#includestdio.h
void Swap(int a, int b); //函数声明int main(void) {int i 3, j 5;Swap(i, j);printf(i %d, j %d\n, i, j);return 0;
}void Swap(int a, int b) {int buf;buf a;a b;b buf;return;
}
大家想一下执行这个程序是否能互换i和j的值不能i还是3, j还是5。以上传递方式叫作拷贝传递即将内存1中的值拷贝到内存2中。拷贝传递的结果是不管如何改变内存2中的值对内存1中的值都没有任何影响因为它们两个是不同的内存空间。
所以要想直接对内存单元进行操控用指针最直接指针的功能很强大。下面改进这个程序
#includestdio.hvoid Swap(int *p, int *q); //函数声明int main(void) {int i 3, j 5;Swap(i, j);printf(i %d, j %d\n, i, j);return 0;
}void Swap(int *p, int *q) {int buf;buf *p;*p *q;*q buf;return;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
i 5, j 3--------------------------------------*/
此时实参向形参传递的不是变量i和j的数据而是变量i和j的地址。其实传递指针也是拷贝传递只不过它拷贝的不是内存单元中的内容而是内存单元的地址这就是天壤之别了。拷贝地址就可以直接对地址所指向的内存单元进行操作即此时被调函数就可以直接对变量i和j进行操作了。
定义只读变量const
const是constant的缩写意思是“恒定不变的”它是定义只读变量的关键字或者说const是定义常变量的关键字说它定义的是变量但又相当于常量说它定义的是常量但又有变量的属性所以叫常变量。用const定义常变量的方法很简单就在通常定义变量时前面加const即可如 const int a 10;
const和变量类型int可以互换位置二者是等价的即上条语句等价于 int const a 10;
用const定义的变量的值是不允许改变的即不允许给它重新赋值即使是赋相同的值也不可以。所以说它定义的是只读变量。而且用const修饰的变量无论是全局变量还是局部变量生存周期都是程序运行的整个过程。而使用const修饰过的局部变量就有了静态特性它的生存周期也是程序运行的整个过程。但是用const修饰过的局部变量只是有了静态特性并没有说它变成了静态变量。我们知道局部变量存储在栈中静态变量存储在静态存储区中而经过const修饰过的变量存储在内存中的“只读数据段”中。只读数据段中存放着常量和只读变量等不可修改的量。
前面说过数组的长度不能是变量。虽然const定义的是只读变量就相当于是定义一个常量。但是只读变量也是变量所以const定义的变量仍然不能作为数组的长度。但是需要注意的是在C中可以C扩展了const的含义在C中用const定义的变量也可作为数组的长度。
多人在学习const的时候都会混淆它与define的区别。从功能上说它们确实很像但它们又有明显的不同
define是预编译指令而const是普通变量的定义。用const定义的常变量具有宏的优点而且使用更方便。所以编程时在使用const和define都可以的情况下尽量使用常变量来取代宏。const定义的是变量而宏定义的是常量所以const定义的对象有数据类型而宏定义的对象没有数据类型。所以编译器可以对前者进行类型安全检查而对后者只是机械地进行字符替换没有类型安全检查。这样就很容易出问题即“边际问题”或者说是“括号问题”。这个问题不是本书讨论的范畴。
用const修饰指针变量时的三种效果
前面讲过当一个变量用const修饰后就不允许改变它的值了。那么如果在定义指针变量的时候用const修饰会怎样同样必须要在定义的时候进行初始化。比如 int a;int *p a;
当用const进行修饰时根据const位置的不同有三种效果。原则是修饰谁谁的内容就不可变其他的都可变。这三种情况在面试的时候几乎是必考的在实际编程中也是经常使用的所以大家必须要掌握。
1const int pa;
同样const和int可以互换位置二者是等价的。我们以放在最前面时进行描述。
当把const放最前面的时候它修饰的就是*p那么*p就不可变。*p表示的是指针变量p所指向的内存单元里面的内容此时这个内容不可变。其他的都可变如p中存放的是指向的内存单元的地址这个地址可变即p的指向可变。但指向谁谁的内容就不可变。
这种用法常见于定义函数的形参。前面学习printf和scanf以及后面将要学习的很多函数它们的原型中很多参数都是用const修饰的。为什么要用const修饰呢这样做的好处是安全我们通过参数传递数据时就把数据暴露了。而大多数情况下我们只是想使用传过来的数据并不想改变它的值但往往由于编程人员个人水平的原因会不小心改变它的值。这时我们在形参中用const把传过来的数据定义成只读的这样就更安全了。这也是const最有用之处。
2int const pa;
此时const修饰的是p所以p中存放的内存单元的地址不可变而内存单元中的内容可变。即p的指向不可变p所指向的内存单元的内容可变。
3const int const pa;
此时*p和p都被修饰了那么p中存放的内存单元的地址和内存单元中的内容都不可变。
综上所述使用const可以保护用指针访问内存时由指针导致的被访问内存空间中数据的误更改。因为指针是直接访问内存的没有拷贝而有些时候使用指针访问内存时并不是要改变里面的值而只是要使用里面的值所以一旦不小心误操作把里面的数据改了就糟糕了。但是这里需要注意的是上面第1种情况中虽然在*p前加上const可以禁止指针变量p修改变量a中的值但是它只能“禁止指针变量p修改”。也就是说它只能保证在使用指针变量p时p不能修改a中的值。但是我并没有说const可以保护a禁止一切的修改其他指向a的没有用const修饰的指针变量照样可以修改a的值而且变量a自己也可以修改自己的值这点一定要记清楚。
指针和一维数组的关系
用指针引用数组元素
引用数组元素可以用“下标法”这个在前面已经讲过也用过了。但是除了这种方法之外还可以用指针即通过指向某个数组元素的指针变量来引用数组元素。数组包含若干个元素元素就是变量变量都有地址。所以每一个数组元素在内存中都占有存储单元都有相应的地址。指针变量既然可以指向变量当然也就可以指向数组元素。同样数组的类型和指针变量的基类型一定要相同。下面给大家写一个程序
#includestdio.h
int main(void) {int a[] {1, 2, 3, 4, 5};int *p a[0];int *q a;printf(*p %d, *q %d\n, *p, *q);return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
*p 1, *q 1
--------------------------------------
*/
C语言中规定“数组名”是一个指针“常量”表示数组第一个元素的起始地址。所以pa[0]和pa是等价的。
指针的移动
C语言规定如果指针变量p已经指向一维数组的第一个元素那么p1就表示指向该数组的第二个元素。
知道元素的地址后引用元素就很简单了。如果p指向的是第一个元素的地址那么*p表示的就是第一个元素的内容。同样pi表示的是第i1个元素的地址那么*(pi)就表示第i1个元素的内容。即pi就是指向元素a[i]的指针*(pi)就等价于a[i]。
那么反过来因为数组名a表示的也是数组的首地址那么元素a[i]的地址也可以用ai表示吗回答也是“可以的”。这时有人说a不是指针变量也可以写成“*”的形式吗只要是地址都可以用“*地址”表示该地址所指向的内存单元中的数据。而且也只有地址前面才能加“*”。
实际上系统在编译时数组元素a[i]就是按*(ai)处理的。即首先通过数组名a找到数组的首地址然后首地址再加上i就是元素a[i]的地址然后通过该地址找到该单元中的内容。所以a[i]写成*(ai)的形式程序的执行效率会更高、速度会更快。
所以建议你们以后在编程的时候除了定义数组时使用数组的形式之外程序中在访问数组元素时全部写成指针*(ai)的形式。这样能够提高程序的执行效率。所以前面的p[i]也不要用就用*(pi)就行了。如果指针还使用p[i]形式就好像给人一种“社会向后倒退发展”的感觉。公司要求编写程序第一个要考虑的就是代码的执行效率这也是数组的一个缺陷。或者直接抛弃数组直接用指针malloc动态分配一块大的内存空间当作数组来用。什么是malloc我们稍后再讲。 指针变量的自增运算
自增就是指针变量向后移自减就是指针变量向前移。下面给大家写一个程序
#includestdio.hint main(void){int a[] {2, 5, 8, 7, 4};int *p a;printf(*p %d, *p %d\n, *p, *p);return 0;}/*在VC 6.0中的输出结果是--------------------------------------p 5, p 5--------------------------------------*/
因为指针运算符“*”和自增运算符“”的优先级相同而它们的结合方向是从右往左所以*p就相当于*(p), *p就相当于*(p)。
如果不用指针用a能用a吗
在前面讲自增和自减的时候强调过只有变量才能进行自增和自减常量是不能进行自增和自减的。a代表的是数组的首地址是一个常量所以不能进行自增所以不能写成a。
指针变量占多少字节
同样用sizeof写一个程序看一下就知道了
#includestdio.hint main(void) {int *a NULL;float *b NULL;double *c NULL;char *d NULL;printf(%d %d %d %d\n, sizeof(a), sizeof(b), sizeof(c), sizeof(d));return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
4 4 4 4
--------------------------------------
*/
可见不管是什么基类型系统给指针变量分配的内存空间都是4字节。指针变量的“基类型”仅用来指定该指针变量可以指向的变量类型并没有其他意思。
注意一个指针在32位的计算机上占4个字节一个指针在64位的计算机上占8个字节。
用64位的电脑跑这个程序结果如下 我们还是接着4字节来讲这个问题-------
那么为什么系统给指针变量分配的是4字节我们前面讲过32位计算机有32根地址线每根地址线要么是0要么是1只有这两种状态。内存单元中的每个地址都是由这32根地址线通过不同的状态组合而成的。而4字节正好是32位正好能存储下所有内存单元的地址信息。少一字节可能就不够多一字节就浪费所以是4字节。 动态内存分配
动态内存是指在堆上分配的内存而静态内存是指在栈上分配的内存。堆上分配的内存是由程序员通过编程自己手动分配和释放的空间很大存储自由。
传统数组的缺点
1数组的长度必须事先指定而且只能是常量不能是变量。
2因为数组长度只能是常量所以它的长度不能在函数运行的过程当中动态地扩充和缩小。
3对于数组所占内存空间程序员无法手动编程释放只能在函数运行结束后由系统自动释放。
所谓“传统数组”的问题实际上就是静态内存的问题。我们讲传统数组的缺陷实际上就是以传统数组为例讲静态内存的缺陷。本质上讲的是以前所有的内存分配的缺陷。正因为它有这么多缺陷所以动态内存就变得很重要。动态数组就能很好地解决传统数组的这几个缺陷。
malloc函数的使用一
那么动态内存是怎么造出来的这个难度就比较大了这是第一个难点。后面讲的“跨函数使用动态内存”是第二个难点而且难度更大。
malloc是一个系统函数它是memory allocate的缩写。其中memory是“内存”的意思allocate是“分配”的意思。所以顾名思义malloc函数的功能就是“分配内存”。要调用它必须要包含头文件stdlib.h。它的原型为
#includestdlib.h
void *malloc(unsigned long size);
malloc函数只有一个形参并且是整型。该函数的功能是在内存的动态存储空间即堆中分配一个长度为size的连续空间。函数的返回值是一个指向所分配内存空间起始地址的指针类型为void *型。说得简单点就是malloc函数的返回值是一个地址这个地址就是动态分配的内存空间的起始地址。如果此函数未能成功地执行如内存空间不足则返回空指针NULL。
“int i5; ”表示分配了4字节的“静态内存”。这里需要强调的是“静态内存”和“静态变量”虽然都有“静态”两个字但是它们没有任何关系。不要以为“静态”变量的内存就是“静态内存”。静态变量的关键字是static它与全局变量一样都是在“静态存储区”中分配的。这块内存在程序编译的时候就已经分配好了而且在程序的整个运行期间都存在而静态内存是在栈中分配的比如局部变量。
那么我们如何判断一个内存是静态内存还是动态内存呢凡是动态分配的内存都有一个标志都是用一个系统的动态分配函数来实现的如malloc或calloc。calloc和malloc的功能很相似我们一般都用malloc。calloc用得很少所以我们不讲。
那么如何用malloc动态分配内存呢比如
int *p (int *)malloc(4);
它的意思是请求系统分配4字节的内存空间并返回第一字节的地址然后赋给指针变量p。在前面讲指针的时候强调了千万不要引用未初始化的指针变量。但是当用malloc分配动态内存之后上面这个指针变量p就被初始化了。
但是下面有一个很关键的问题函数malloc的返回值类型为void *型而指针变量p的类型是int *型即两个类型不一样那么可以相互赋值吗上面语句是将void *型“强制类型转换”成int *型但事实上可以不用转换。在C语言中void *型可以不经转换地直接赋给任何类型的指针变量函数指针变量除外。实际上也是经过转换的只不过是系统自动转换的。当然在C语言中强制转换malloc()的返回值并没有错只是没有必要。因此在C语言中不推荐强制类型转换malloc()的返回值。但是在C中如果要使用malloc()函数那么必须要进行强制类型转换否则编译时就会出错。但是在C中我们一般也不会使用malloc()而是使用new。
所以“int *p(int *)malloc(4); ”就可以写成“int *pmalloc(4); ”。
这时有人会问“void不是不会有返回值吗为什么malloc还会有返回值”这里需要注意的是malloc函数的返回值类型是void *型而不是void型。void *型和void型是有区别的。void *型是定义一个无类型的指针变量它可以指向任何类型的数据。但是需要注意的是不能对void *型的指针变量进行运算操作如指针的运算、指针的移动等。
下面利用“int *pmalloc(4); ”语句给大家写一个很有意思的程序
#includestdio.h
#includestdlib.h
int main(void) {while (1) {int *p malloc(1000);}return 0;
}
这个程序是非常简单的一个木马病毒。这个程序有一个专业的名称叫“内存泄漏”。什么是“内存泄漏”呢每台电脑都有内存所有的程序都是先存放到内存里面才能运行。但是上面这个程序将内存空间都占满了那么其他程序就没有地方存放了所以内存就好像泄漏了一样。
malloc函数的使用二
下面使用malloc函数写一个程序程序的功能是调用被调函数将主调函数中动态分配的内存中的数据放大10倍。
#includestdio.h
#includestdlib.h
void Decuple(int *i); //函数声明decuple是10倍的意思int main(void) {int *p malloc(4);*p 10;Decuple(p);printf(*p %d\n, *p);return 0;
}void Decuple(int *i) {*i (*i) * 10;return;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
p 100
--------------------------------------
*/
这个程序有一个地方需要说明即“int *pmalloc(4); ”。4表示分配了4字节的动态内存。但是这是因为在前面用sizeof试过笔者的计算机其给int型变量分配的是4字节所以这么写没有问题。但是如果把这个程序移植到其他计算机中呢系统给int型变量分配的还是4字节吗所以直接写“4”的可移植性很差。如果别的计算机给int型变量分配的是8字节这时候如果还写“int *pmalloc(4); ”代码也能通过编译但是会有4字节因“无家可归”而直接“住进邻居家”。造成的后果是后面内存中的原有数据被覆盖。下面写一个程序验证一下
#includestdio.h
#includestdlib.hint main(void) {int *p malloc(1); //分配了1字节的动态内存空间*p 1000;printf(*p %d\n, *p);return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
p 1000
--------------------------------------
*/
malloc动态分配了1字节的内存空间最多能存放的整数是255。但现在将1000赋给p竟然不警告而且还能输出1000这说明超过一字节的数据肯定住到“邻居”家去了。这样“邻居”家的数据就被覆盖了。
也就是说int型变量并不是一定占4字节。C语言从来没有规定一个整型必须要强制占几字节。C语言只规定了短整型的长度不能大于整型整型的长度不能大于长整型。至于具体占多少字节不同的计算机是有差别的这主要由计算机的操作系统决定或者说由安装在该系统上的编译器的编译规则决定。所以malloc后面直接写“4”不是很好最好的方式为sizeof(int)即 int *p malloc(sizeof(int));
sizeof(int)的值是int型变量所占的字节数这样就能很好地表示当前计算机中int型变量占几字节。这样写程序的可移植性就增强了。所以动态内存最好是需要多少就构建多少。多了虽然不会有问题但是会浪费内存而少了就可能出问题。
如果还想简单一点的话也可以像下面这样写 int *p malloc(sizeof*p);
前面讲过sizeof的后面可以紧跟类型也可以直接跟变量名。如果是变量名那么就表示该变量在内存中所占的字节数。所以*p是int型的那么sizeof*p就表示int型变量在内存中所占的字节数。而且这样连sizeof后面的括号都可以省略了当然加上括号也行。但是如果写类型的话sizeof(int)中int两端的括号就不能省略。此外如果写变量名那么可以不加括号但是必须要用空格隔开。但是如果变量名前面有“*”比如“sizeof*p”那么空格也可以不加当然加上也可以。笔者习惯不加因为这样sizeof和*p看上去更像一个整体。
free函数的使用
前面讲过动态分配的内存空间是由程序员手动编程释放的。那么怎么释放呢是用free函数。free函数的原型是
#includestdio.hvoid free(void *p);
free函数无返回值。它的功能是释放指针变量p所指向的内存单元。所谓释放并不是指清空内存空间而是指将该内存空间标记为“可用”状态使操作系统在分配内存时可以将它重新分配给其他变量使用。但是需要注意的是指针变量p被释放之后它仍然是指向那块内存空间的只是那块内存空间已经不再属于它了而已。如果其他变量在里面存放了值而你现在用p往里面写入数据就会把那个值给覆盖这样就会造成其他程序错误。所以当指针变量被释放后要立刻把它的指向改为NULL。
那么当指针变量被释放后它所指向的内存空间中的数据会怎样呢是被清空了还是仍然是原来那个值呢或是存放一个极小的负数这个不一定free的标准行为只是表示这块内存可以被再分配至于它里面的数据是否被清空并没有强制要求。不同的编译器处理的方式可能不一样这个不需要我们考虑。如果是在VC 6.0中当指针变量被释放后虽然它仍然是指向那个内存空间的但那个内存空间中的值将会被重新置一个非常小的负数。这个大家了解一下就行了编程中不需要我们考虑这个问题而且在不同的编译器中结果可能都不一样。
malloc和free一定要成对存在一一对应。有malloc就一定要有free有几个malloc就要有几个free。而且free和置NULL也一定要成对存在也就是说释放一个指向动态内存的指针变量后要立刻把它指向NULL。
最后需要注意的是对传统数组而言对数组名使用sizeof可以求出整个数组在内存中所占的字节数即可以求出数组的长度。但是对动态数组而言这么做是行不通的因为动态数组是通过指针变量引用的而对指针变量使用sizeof结果都是4所以无法通过sizeof求出整个动态数组的长度。不过动态数组的长度也不需要通过sizeof去求得因为动态数组的长度是可以动态指定的即可以是变量这个变量表示的就是动态数组的长度。
动态数组长度的扩充和缩小
动态数组的长度可以在函数运行的过程当中动态地扩充和缩小。怎么扩充和缩小用realloc函数。realloc函数也是系统提供的系统函数它是英文单词reallocate的缩写即“重新分配”的意思。该函数的原型为
#includestdlib.hvoid *realloc(void *p, unsigned long size);
其中指针变量p是指向“要改变内存大小的动态内存的”指针变量。指针变量p是void *型的表示可以改变任何基类型的、指向动态内存的指针变量。第二个参数size是重新指定的“新的长度”。
新的长度”可大可小但是要注意如果“新的长度”小于原内存的大小可能会导致数据丢失慎用如果是扩充的话原有数据仍然被保留着仅在已有内存的基础上进行扩充。比如现在是5字节扩充到7字节那么原来的5个内存单元不动里面的数据也不会改变只在原来的基础上增加2个内存单元。
动态数组长度的扩充和缩小大家了解一下就行了不是我们学习的重点在实际编程中用得也不多。
通过指针引用二维数组
要理解指针和二维数组的关系首先要记住一句话二维数组就是一维数组。如果理解不了这句话那么你就无法理解指针和二维数组的关系。
假如有一个二维数组 int a[3][4] {{1, 3, 5, 7}, {9, 11, 13, 15}, {17, 19, 21, 23}};
其中a是二维数组名。a数组包含3行即3个行元素a[0], a[1], a[2]。每个行元素都可以看成含有4个元素的一维数组。而且C语言规定a[0]、a[1]、a[2]分别是这三个一维数组的数组名。如下所示 a[0]、a[1]、a[2]既然是一维数组名一维数组的数组名表示的就是数组第一个元素的地址所以a[0]表示的就是元素a[0][0]的地址即a[0]a[0][0]; a[1]表示的就是元素a[1][0]的地址即a[1]a[1][0]; a[2]表示的就是元素a[2][0]的地址即a[2]a[2][0]。
我们知道在一维数组b中数组名b代表数组的首地址即数组第一个元素的地址b1代表数组第二个元素的地址…, bn代表数组第n1个元素的地址。所以既然a[0]、a[1]、a[2]、…、a[M -1]分别表示二维数组a[M][N]第0行、第1行、第2行、…、第M-1行各一维数组的首地址那么同样的道理a[0]1就表示元素a[0][1]的地址a[0]2就表示元素a[0][2]的地址a[1]1就表示元素a[1][1]的地址a[1]2就表示元素a[1][2]的地址……a[i]j就表示a[i][j]的地址。 二维数组的首地址和数组名
下面来探讨一个问题“二维数组a[M][N]的数组名a表示的是谁的地址”在一维数组中数组名表示的是数组第一个元素的地址那么二维数组呢a表示的是元素a[0][0]的地址吗不是我们说过二维数组就是一维数组二维数组a[3][4]就是有三个元素a[0]、a[1]、a[2]的一维数组所以数组a的第一个元素不是a[0][0]而是a[0]所以数组名a表示的不是元素a[0][0]的地址而是a[0]的地址即 a a[0]
而a[0]又是a[0][0]的地址即 a[0] a[0][0]
所以二维数组名a和元素a[0][0]的关系是 a (a[0][0])
即二维数组名a是地址的地址必须两次取值才可以取出数组中存储的数据。对于二维数组a[M][N]数组名a的类型为int (*)[N]所以如果定义了一个指针变量p int *p;
并希望这个指针变量指向二维数组a那么不能把a赋给p因为它们的类型不一样。要么把a[0][0]赋给p要么把a[0]赋给p要么把*a赋给p。前两个好理解可为什么可以把*a赋给p因为a(a[0][0])所以*a*((a[0][0]))a[0][0]。除此之外你也可以把指针变量p定义成int (*)[N]型这时就可以把a赋给p而且用这种方法的人还比较多。
下面写个程序
#includestdio.h
int main(void)
{int arr[2][2]{{1,2},{3,4}};int (*p)[2]arr;for(int i0;i2;i){for(int j0;j2;j){printf(%-2d\x20,*(*(pi)j));//注意写法 }}return 0;
}
再来看如果把a[0][0]赋给指针变量p的话会有如下规律 pi4j a[i][j] 其中4是二维数组的列数。
写个程序验证一下
#includestdio.h
int main(void) {int a[3][4] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};int i, j;int *p a[0][0]; //把a[0][0]的地址赋给指针变量pfor (i0; i3; i) {for (j0; j4; j) {printf(%-2d\x20, *(pi*4j));}printf(\n);}return 0;
}
/*在VC 6.0中的输出结果是
--------------------------------------
1 2 3 4
5 6 7 8
9 10 11 12
--------------------------------------
*/ 函数指针
如果在程序中定义了一个函数那么在编译时系统就会为这个函数代码分配一段存储空间这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放这个指针变量就叫作函数指针变量简称函数指针。
那么这个指针变量怎么定义呢虽然同样是指向一个地址但指向函数的指针变量同我们之前讲的指向变量的指针变量的定义方式是不同的。例如 int(*p)(int, int);
这个语句就定义了一个指向函数的指针变量p。首先它是一个指针变量所以要有一个“*”即(*p)其次前面的int表示这个指针变量可以指向返回值类型为int型的函数后面括号中的两个int表示这个指针变量可以指向有两个参数且都是int型的函数。
所以函数指针的定义方式为 函数返回值类型 (* 指针变量名) (函数参数列表);
如何用函数指针调用函数
给大家举一个例子 int Func(int x); /声明一个函数/int (*p) (int x); /定义一个函数指针/p Func; /将Func函数的首地址赋给指针变量p/
赋值时函数Func不带括号也不带参数。由于函数名Func代表函数的首地址因此经过赋值以后指针变量p就指向函数Func()代码的首地址了。
下面来写一个程序看了这个程序你们就明白函数指针怎么使用了
#includestdio.hint Max(int, int); //函数声明int main(void) {int(*p)(int, int); //定义一个函数指针int a, b, c;p Max; //把函数Max赋给指针变量p使p指向Max函数printf(please enter a and b:);scanf(%d%d, a, b);c (*p)(a, b); //通过函数指针调用Max函数printf(a %d\nb %d\nmax %d\n, a, b, c);return 0;
}int Max(int x, int y) { //定义Max函数int z;if (x y) {z x;} else {z y;}return z;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
please enter a and b:3 4
a 3
b 4
max 4
--------------------------------------
*/ 字符串
C语言规定在每一个字符串常量的结尾系统都会自动加一个字符\0作为该字符串的“结束标志符”系统据此判断字符串是否结束。这里要特别强调一点 \0是系统自动加上的不是人为添加的。\0是ASCII码为0的字符它不会引起任何控制动作也不是一个可以显示的字符。
在字符串常量中如果“双撇号”中能看见的字符有n个那么该字符串在内存中所占的内存空间为n1字节。但是C语言规定1个英文字符占1字节而1个中文字符占2字节就算是中文的标点符号也是占2字节。
不能将一个字符串常量赋给一个字符变量
第一我们在前面讲过字符变量用char定义。一个字符变量中只能存放一个字符。而字符串一般都有好多字符占多字节。所以不能将多个字符赋给只占一字节的变量。那么如果字符串常量的双撇号内什么都不写此时就只有一个字符\0那么此时可不可以将它赋给字符变量不可以原因看下面第二点。
第二字符串是指一系列字符的组合。在C语言中字符变量的类型用char定义。我们这里讲的是数据类型但是字符串不属于数据类型也就不存在字符串变量。一种类型的变量要想存储某个对象必须能兼容该对象的数据类型而字符串连数据类型都算不上又怎么能将它赋给字符变量呢所以在C语言中任何数据类型都不可以直接存储一个字符串。那么字符串如何存储在C语言中字符串有两种存储方式一种是通过字符数组存储另一种是通过字符指针存储。
这里需要注意的是虽然C语言里面没有数据类型可以存储字符串但C和Java中都有。
字符数组
因为字符数组首先是一个数组所以前面讲的数组内容通通都适用。其次它是存放字符的数组即数组的类型是char型。比如 char name[10];
表示定义了10字节的连续内存空间。
1如果字符串的长度大于10那么就存在语法错误。这里需要注意的是这里指的“字符串的长度”包括最后的\0。也就是说虽然系统会自动在字符串的结尾加\0但它不会自动为\0开辟内存空间。所以在定义数组长度的时候一定要考虑\0。
2如果字符串的长度小于数组的长度则只将字符串中的字符赋给数组中前面的元素剩下的内存空间系统会自动用\0填充。
字符与数组的初始化字符数组的初始化与前面所讲数组的初始化一样要么定义时初始化要么定义后初始化。先定义后初始化必须一个一个地进行赋值不能整体赋值
前面讲过系统会在字符串的最后自动添加结束标志符\0但是当一个一个赋值时系统不会自动添加\0必须手动添加。如果忘记添加虽然语法上没有错误但是程序将无法达到我们想要的功能。此外空格字符必须要在单引号内“敲”一个空格不能什么都不“敲”什么都不“敲”就是语法错误。也不能多“敲”因为一个单引号内只能放一个字符“敲”多个空格就是多个字符了。
假如数组b最后没有手动添加\0的。程序是希望数组b输出“i miss you”但输出结果是“i miss you烫i love you”。原因就是系统没有在最后添加\0。虽然程序中对数组b的长度进行了限制即长度为10但是由于内存单元是连续的对于字符串系统只要没有遇到\0就会认为该字符串还没有结束就会一直往后找直到遇到\0为止。被找过的内存单元都会输出从而超出定义的10字节。
定义时初始化可以整体赋值。整体赋值有一个明显的优点——方便。定义时初始化可以不用指定数组的长度而先定义后初始化则必须要指定数组的长度。
汉字不能分开一个一个赋值。因为一个汉字占2字节若分开赋值由于一个单引号内只能放一个字符即一字节所以将占2字节的汉字放进去当然就出错了。因此如果用字符数组存储汉字的话必须整体赋值即要么定义时初始化要么调用strcpy函数。
下面写一个程序巩固一下
#includestdio.hint main(void){char str[3] ;str[2] a ;printf(str %s\n, str);return 0;}/*在VC 6.0中的输出结果是--------------------------------------str --------------------------------------*/
程序中定义了一个长度为3的字符数组然后给第三个元素赋值为a然后将整个字符数组输出。但是输出结果什么都没有原因就是其直接初始化为一对双引号此时字符数组中所有元素都是\0。所以虽然第三个元素为a但因为第一个元素为\0而\0是字符串的结束标志符所以无法输出。需要注意的是使用此种初始化方式时一定要指定数组的长度否则默认数组长度为1。
总结字符数组与前面讲的数值数组有一个很大的区别即字符数组可以通过“%s”一次性全部输出而数值数组只能逐个输出每个元素。
初始化内存函数memset()
在前面不止一次说过定义变量时一定要进行初始化尤其是数组和结构体这种占用内存大的数据结构。在使用数组的时候经常因为没有初始化而产生“烫烫烫烫烫烫”这样的野值俗称“乱码”。每种类型的变量都有各自的初始化方法memset()函数可以说是初始化内存的“万能函数”通常为新申请的内存进行初始化工作。它是直接操作内存空间mem即“内存”memory的意思。该函数的原型为
#includestring.hvoid *memset(void *s, int c, unsigned long n);
函数的功能是将指针变量s所指向的前n字节的内存单元用一个“整数”c替换注意c是int型。s是void *型的指针变量所以它可以为任何类型的数据进行初始化。
memset()的作用是在一段内存块中填充某个给定的值。。memset一般使用“0”初始化内存单元而且通常是给数组或结构体进行初始化。一般的变量如char、int、float、double等类型的变量直接初始化即可没有必要用memset。如果用memset的话反而显得麻烦。当然数组也可以直接进行初始化但memset是对较大的数组或结构体进行清零初始化的最快方法因为它是直接对内存进行操作的。
这时有人会问“字符串数组不是最好用\0进行初始化吗那么可以用memset给字符串数组进行初始化吗也就是说参数c可以赋值为\0吗”可以的虽然参数c要求是一个整数但是整型和字符型是互通的。但是赋值为\0和0是等价的因为字符\0在内存中就是0。所以在memset中初始化为0也具有结束标志符\0的作用所以通常我们就写“0”。
memset函数的第三个参数n的值一般用sizeof()获取这样比较专业。注意如果是对指针变量所指向的内存单元进行清零初始化那么一定要先对这个指针变量进行初始化即一定要先让它指向某个有效的地址。而且用memset给指针变量如p所指向的内存单元进行初始化时n千万别写成sizeof(p)这是新手经常会犯的错误。因为p是指针变量不管p指向什么类型的变量sizeof(p)的值都是4。
下面写一个程序
#includestdio.h
#includestring.hint main(void) {int i; //循环变量char str[10];char *p str;memset(str, 0, sizeof(str)); //只能写sizeof(str)不能写sizeof(p)for (i0; i10; i) {printf(%d\x20, str[i]);}printf(\n);return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------------------------------------------------
memset(p, 0, sizeof(p)); //地址的大小都是4字节
0 0 0 0 -52 -52 -52 -52 -52 -52
--------------------------------------------------------------------------------
memset(p, 0, sizeof(p)); //*p表示的是一个字符变量只有一字节
0 -52 -52 -52 -52 -52 -52 -52 -52 -52
--------------------------------------------------------------------------------
memset(p, 0, sizeof(str));
0 0 0 0 0 0 0 0 0 0
--------------------------------------------------------------------------------
memset(str, 0, sizeof(str));
0 0 0 0 0 0 0 0 0 0
--------------------------------------------------------------------------------
memset(p, 0, 10); //直接写10也行但不专业
0 0 0 0 0 0 0 0 0 0
--------------------------------------------------------------------------------
*/
用scanf输入字符串
除了在定义字符数组时初始化外还可以通过scanf从键盘输入字符串。下面写一个程序
#includestdio.h
int main(void) {char str[10]; //str是string的缩写即字符串printf(请输入字符串);scanf(%s, str); /*输入参数是已经定义好的“字符数组名”不用加因为在C语言中数组名就代表该数组的起始地址*/printf(输出结果%s\n, str);return 0;
}
/*
在VC 6.0中的输出结果是
----------------------------------------
请输入字符串爱你一生一世
输出结果爱你一生一世
----------------------------------------
*/
用scanf给字符数组赋值不同于对数值型数组赋值。前面讲过给数值型数组赋值时只能用for循环一个一个地赋值不能整体赋值。而给字符数组赋值时可以直接赋值不需要使用循环。此外从键盘输入后系统会自动在最后添加结束标志符\0。但是用scanf输入字符串时有一个地方需要注意如果输入的字符串中带空格比如“i love you”那么就会有一个问题。我们将上面程序运行时输入的字符串改一下
#includestdio.h
int main(void) {char str[10]; //str是string的缩写即字符串printf(请输入字符串);scanf(%s, str); /*输入参数是已经定义好的“字符数组名”不用加因为在C语言中数组名就代表该数组的起始地址*/printf(输出结果%s\n, str);return 0;
}
/*
在VC 6.0中的输出结果是
----------------------------------------
请输入字符串i love you
输出结果i
----------------------------------------
*/
我们看到输入的是“i love you”而输出的只有“i”。原因是系统将空格作为输入字符串之间的分隔符。也就是说只要一“敲”空格系统就认为当前的字符串已经结束接下来输入的是下一个字符串所以只会将空格之前的字符串存储到定义好的字符数组中。那么这种情况该怎么办那么就以空格为分隔符数数有多少个字符串有多少个字符串就定义多少个字符数组。比如“i love you”有两个空格表示有三个字符串那么就定义三个字符数组
#includestdio.hint main(void) {char str1[10], str2[10], str3[10];printf(请输入字符串);scanf(%s%s%s, str1, str2, str3);printf(输出结果%s %s %s\n, str1, str2, str3); //%s间要加空格return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
请输入字符串i love you
输出结果i love you
--------------------------------------
*/
最后还有一点需要说明我们在前面讲“清空缓冲区”的时候讲过用scanf输入时不管输入什么最后“敲”的回车都会被留在缓冲区这里也不例外。输入字符串时最后“敲”的回车也会被留在缓冲区如果紧接着要给一个字符变量赋值的话那么还没等你输入系统就自动退出来了。因为系统自动将回车产生的字符\n赋给该字符变量了所以此时对字符变量赋值前要首先清空缓冲区。
字符串与指针
在C语言中有两种方法存储和访问一个字符串一是用字符数组二是用字符指针指向一个字符串。字符指针首先是一个指针变量所以要有“指针运算符*”其次指针变量里面存放的是地址这一点一定要明确最后它是字符所以是char型。
下面写一个程序
#includestdio.hint main(void) {char *string I Love You Mom! ;printf(%s\n, string); //输出参数是已经定义好的“指针变量名”return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
I Love You Mom!
--------------------------------------
*/
这个程序并没有定义一个字符数组而是定义了一个字符指针变量string。C语言对字符串常量是按字符数组处理的即在内存中开辟了一个字符数组用来存放字符串常量。程序中对字符指针变量string的初始化实际上是把系统为字符串I Love You Mom! 在内存中开辟的字符数组的第一个元素的地址赋给了string。即string中存放的是字符I的地址。
输出参数要写“字符指针变量名”这样系统在输出时会首先输出该指针变量所指向的字符即I然后string自动加1使之指向下一个字符……直到遇到字符串结束标志\0为止。同样 \0是系统自动添加的。
如何用scanf给字符指针变量所指向的内存单元初始化
前面在讲指针的时候专门讲过scanf的问题。首先要明确的一点是scanf只能给字符指针变量所指向的内存单元初始化不能给字符指针变量初始化。其次在用scanf给字符指针变量所指向的内存单元初始化之前一定要先使字符指针变量明确地指向某个具体的字符数组。下面写一个程序
#includestdio.h
int main(void) {char str[30];char *string str; //一定要先给字符指针变量初始化printf(请输入字符串);scanf(%s, string);printf(%s\n, string); //输出参数是已经定义好的“指针变量名”return 0;
}
/*
在VC 6.0中的输出结果是
------------------------------------------------------
请输入字符串Hi清明节一起去玩啊
Hi清明节一起去玩啊
------------------------------------------------------
*/
字符串处理函数
字符串输入函数gets()
在前面从键盘输入字符串是使用scanf和%s。其实还有更简单的方法即使用gets()函数。该函数的原型为
#includestdio.h
char *gets(char *str);
gets()函数的功能是从输入缓冲区中读取一个字符串存储到字符指针变量str所指向的内存空间。就算输入的字符串中有空格也可以直接输入不用像scanf那样要定义多个字符数组。、
关于使用gets()函数需要注意使用gets()时系统会将最后“敲”的换行符从缓冲区中取出来然后丢弃所以缓冲区中不会遗留换行符。这就意味着如果前面使用过gets()而后面又要从键盘给字符变量赋值的话就不需要吸收回车清空缓冲区了因为缓冲区的回车已经被gets()取出来扔掉了。
优先使用fgets()函数
虽然用gets()时有空格也可以直接输入但是gets()有一个非常大的缺陷即它不检查预留存储区是否能够容纳实际输入的数据这样很不安全。如果输入的字符数目大于数组的长度就会发生内存越界所以编程时建议使用fgets()。
fgets()的原型为
#includestdio.h
char *fgets(char *s, int size, FILE *stream);
fgets()虽然比gets()安全但安全是要付出代价的代价就是它的使用比gets()要麻烦一点——有三个参数。它的功能是从stream流中读取size个字符存储到字符指针变量s所指向的内存空间。它的返回值是一个指针指向字符串中第一个字符的地址。
其中s代表要保存到的内存空间的首地址可以是字符数组名也可以是指向字符数组的字符指针变量名。size代表的是读取字符串的长度。stream表示从何种流中读取可以是标准输入流stdin也可以是文件流即从某个文件中读取这个在后面讲文件的时候再详细介绍。标准输入流就是前面讲的输入缓冲区。所以如果是从键盘读取数据的话就是从输入缓冲区中读取数据即从标准输入流stdin中读取数据所以第三个参数为stdin。
下面写一个程序
#includestdio.hint main(void) {char str[20]; /*定义一个最大长度为19末尾是\0的字符数组来存储字符串*/printf(请输入一个字符串);fgets(str, 7, stdin); /*从输入流stdin即输入缓冲区中读取7个字符到字符数组str中*/printf(%s\n, str);return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
请输入一个字符串i love you
i love
--------------------------------------
*/
我们发现输入的是“i love you”而输出只有“i love”。原因是fgets()只指定了读取7个字符放到字符数组str中。“i love”加上中间的空格和最后的\0正好是7个字符。那有人会问“用fgets()是不是每次都要去数有多少个字符呢这样不是很麻烦吗”不用数fget()函数中的size如果小于字符串的长度那么字符串将会被截取如果size大于字符串的长度则多余的部分系统会自动用\0填充。所以假如你定义的字符数组长度为n那么fgets()中的size就指定为n-1留一个给\0就行了。
但是需要注意的是如果输入的字符串长度没有超过n-1那么系统会将最后输入的换行符\n保存进来保存的位置是紧跟输入的字符然后剩余的空间都用\0填充。所以此时输出该字符串时printf中就不需要加换行符\n了因为字符串中已经有了。下面写一个程序看一下
#includestdio.hint main(void) {char str[30];char *string str; //一定要先给指针变量初始化printf(请输入字符串);fgets(string, 29, stdin); //size指定为比字符数组元素少一就行了printf(%s, string); //printf中不需要添加\n因为字符串中已经有了return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------------------
请输入字符串i love studying C语言
i love studying C语言
--------------------------------------------------
*/
我们看到printf中没有添加换行符\n输出时也自动换行了。所以fgets()和gets()一样最后的回车都会从缓冲区中取出来。只不过gets()是取出来丢掉而fgets()是取出来自己留着。
使用gets()和fgets()前注意吸收回车
在前面介绍清空输入缓冲区时讲过当使用gets()或fgets()给字符数组赋值时如果前面使用过scanf那么scanf遗留的回车将会被它们取出并赋给该字符串并且只能获取这个回车符从而导致字符数组赋值失败。
我们要习惯在每个scanf后都要加上getchar()或“while (getchar()! \n ); ”不管是否需要。在后面的程序中可能有些地方不会添加但一定要养成这个习惯。
字符串输出函数puts()
前面在输出字符串时都使用printf通过“%s”输出字符串。其实还有更简单的方法就是使用puts()函数。该函数的原型为
#includestdio.h
int puts(const char *s);
这个函数也很简单只有一个参数。s可以是字符指针变量名、字符数组名或者直接是一个字符串常量。功能是将字符串输出到屏幕。输出时只有遇到\0也就是字符串结束标志符才会停止。
使用puts()函数连换行符\n都省了使用puts()显示字符串时系统会自动在其后添加一个换行符。但是puts()和printf相比也有一个小小的缺陷就是如果puts()后面的参数是字符指针变量或字符数组那么括号中除了字符指针变量名或字符数组名之外什么都不能写。下面要讲解的fputs()也不可以。
字符串输出函数fputs()
fputs()函数也是用来显示字符串的它的原型是
#includestdio.h
int fputs(const char *s, FILE *stream);
s代表要输出的字符串的首地址可以是字符数组名或字符指针变量名。stream表示向何种流中输出可以是标准输出流stdout也可以是文件流。
fputs()和puts()有两个小区别
1puts()只能向标准输出流输出而fputs()可以向任何流输出。
2使用puts()时系统会在自动在其后添加换行符而使用fputs()时系统不会自动添加换行符。
那么这是不是意味着使用fputs()时就要在后面添加一句“printf(\n); ”换行呢看情况如果输入时使用的是gets()那么就要添加printf换行但如果输入时用的是fgets()则不需要。因为使用gets()时gets()会将回车读取出来并丢弃所以换行符不会像scanf那样被保留在缓冲区也不会被gets()存储而使用fgets()时换行符会被fgets()读出来并存储在字符数组的最后这样当这个字符数组被输出时换行符就会被输出并自动换行。但是也有例外比如使用fgets()时指定了读取的长度如只读取5个字符事实上它只能存储4个字符因为最后还要留一个空间给\0而你却从键盘输入了多于4个字符那么此时“敲”回车后换行符就不会被fgets()存储。数据都没有地方存放哪有地方存放换行符呢此时因为fgets()没有存储换行符所以就不会换行了。
虽然gets()、fgets()、puts()、fput()都是字符串处理函数但它们都包含在stdio.h头文件中并不是包含在string.h头文件中。
字符串复制函数strcpy()
两个字符串变量不可以使用“”进行直接赋值只能通用strcpy()函数进行赋值。strcpy是string copy的缩写即“字符串复制”。它的原型是
#includestring.h
char *strcpy(char *dest, const char *src);
功能是将指针变量src所指向的字符串复制到指针变量dest所指向的位置。dest是destination的缩写即“目的地”; src是source的缩写即“源”。
注意要想用strcpy()将一个字符串复制到一个字符数组中那么字符数组在定义时长度一定要够大要足以容纳被复制的字符串。如果不够大程序运行时就会出错。同样在定义字符数组长度时一定要将结束标志符\0考虑进去系统是不会自动为\0分配内存单元的。
下面写一个程序看一下
#includestdio.h
#includestring.h
int main(void) {char str[20];int i;strcpy(str, i love you);strcpy(str, Lord);for (i 0; i11; i) {printf(%c, str[i]);}printf(\n);return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
Lord e you
--------------------------------------
*/
该程序先将字符串“i love you”赋给数组str然后再把字符串“Lord”赋给数组str。字符串“Lord”总共占5字节的内存单元包括最后的\0。从最后的输出结果可以看出字符串“Lord”分别用字符L 、 o 、 r 、 d 、 \0覆盖了“i love you”的前5个内存单元i 、 、 l 、 o 、 v。从第6个内存单元往后仍保存着原来的数据即e 、 、 y 、 o 、 u。
为什么用printf和puts()无法直接输出上面这个结果为什么要使用循环逐个输出字符因为如果使用printf或者puts()那么输出的只有“Lord”。原因是用printf或者puts()输出字符串时只要遇到\0就会停止输出“Lord”最后正好有一个\0所以后面的e 、 、 y 、 o 、 u就不会输出了。
字符串复制函数strncpy()
strncpy()函数和strcpy()很相似就中间多了一个“n”。事实上它们的功能也是相似的。strncpy()函数的原型为
#includestring.h
char *strncpy(char *dest, const char *src, unsigned long n);
strcpy()的功能是将指针变量src所指向的字符串复制到指针变量dest所指向的位置。而strncpy()的功能是将指针变量src所指向的字符串的前n个字符复制到指针变量dest所指向的位置。只要将strcpy()函数掌握之后strncpy()就很简单了。关于strncpy()唯一需要注意的是如果它不是复制整个字符串那么最后的结束标志符\0就不会被复制这时候必须手动编程在后面添加\0否则输出时由于找不到结束标志符就会输出乱码。它会一直输出直到遇到\0为止。
内存拷贝函数memcpy()
strcpy()的对象只能是字符串但是memcpy()可以是任何类型的数据因为它是内存拷贝函数是直接对内存进行操作的。该函数的原型为
#includestring.h
void *memcpy(void *dest, const void *src, unsigned long n);
功能是从指针变量src所指向的内存空间中复制n字节的数据到指针变量dest所指向的内存空间中。
其前两个参数的类型都是void *型所以该函数可以将任何类型的数据复制给任何类型的变量。但是在编程的时候最好前两个参数的类型是一致的虽然不一致也不会报错但是那么做没有意义而且往往也达不到想要的效果。
memcpy()可以在任何类型的数据间进行复制但是用得更多的还是复制字符串。因为字符数组无法直接赋给字符数组而其他变量都可以直接赋值所以其他变量没有必要使用memcpy()。
当使用memcpy()复制字符串的时候需要注意以下几点
1字符数组dest的长度一定要大于复制的字节数n否则将会产生溢出导致相邻内存空间的数据被覆盖这样很危险。
2如果复制的是完整的字符串那么字符数组dest的长度和复制的字节数n一定要考虑最后的结束标志符\0。
3如果不是完整复制一个字符串而是仅复制前几个字符那么最后的结束标志符\0就不会被复制。这时在输出dest的时候因为找不到结束标志符\0就会一直往后输出直到遇到\0为止。为此需要通过编程的方式人为添加\0。怎么添加呢就是在定义字符数组dest时直接给它初始化为\0或用memset()给它初始化为0或\0。我们一直强调在定义变量时一定要进行初始化这是良好的编程习惯。
下面写一个程序
#includestdio.h
#includestring.h
int main(void) {char src[20] i love you;char dest[20] \0; //养成初始化的习惯memcpy(dest, src, 19); /*dest的长度要大于n所以n就指定比dest小1就行了*/printf(dest %s\n, dest);return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
dest i love you
--------------------------------------
*/
sprintf()
sprintf()是非常强大的字符串处理函数。它与printf相比就是前面多了一个“s”。“s”就表示字符串即string说明它是为字符串而“生”的。我们知道printf是向屏幕输出其实sprintf()和printf的原理和用法几乎是一样的但是你绝对想不到sprintf()是给一个字符数组赋值的而且还不单单是赋值那么简单其功能更加丰富。sprintf()函数的原型为
#includestdio.h
int sprintf(char *str, 输出控制符, 输出参数);
sprintf()和printf在用法上几乎一模一样只是“打印”的目的地不同而已。printf是“打印”到屏幕上而sprintf()是“打印”到字符串中。这直接导致sprintf()比printf有用得多。sprintf()可以将任何类型的输出参数输出到字符串str中仅此一点我们就能感受到它的强大。sprintf()的强大功能很少会让我们失望。但这里需要注意的是当把一个字符数组中的内容“打印”到str中后该字符数组中的内容并不会消失即相当于是复制式的读取而不是剪切式的。sprintf()最常见的应用莫过于把数字“打印”到字符串中下面写一个程序
#includestdio.h
int main(void) {int i 12345;float j 3.14159;char str1[20];char str2[20];char str3[20];char str4[20];char str5[20];sprintf(str1, %d, i);sprintf(str2, %.5f, j); /*如果不加“.5”的话因为float默认保存到小数点后六位所以最后会输出3.141590*/sprintf(str3, %d%.5f, i, j);sprintf(str4, %d, 123456);sprintf(str5, %d%d, 123, 456); /*如果%d之间加逗号的话这个逗号也会打印出来所以如果你想原样输入的话不要加任何非输出控制符*/printf(str1 %s\nstr2 %s\nstr3 %s\nstr4 %s\nstr5 %s\n, str1, str2,str3, str4, str5);return 0;
}
/*
在VC 6.0中的输出结果是
--------------------------------------
str1 12345
str2 3.14159
str3 123453.14159
str4 123456
str5 123456
--------------------------------------
*/
sprintf()具有强大的字符串处理功能
1sprintf()既然可以将任何类型的输出参数赋给字符数组str当然也可以将字符指针变量、字符数组或字符串常量赋给str即相当于strcpy()。
2如上面程序可以有多个输出控制符从而将多个数字连起来然后赋给字符数组str。所以当然也可以将多个字符串连起来赋给str即相当于strcat()。在许多场合sprintf()可以替代strcat()但sprintf()比strcat()更强大。因为sprintf()可以连接多个字符串而strcat()只能连接两个字符串。strcat()本书未涉及因为用得很少而且很简单就是将两个字符串连接起来。
3sprintf()的返回值返回本次调用“打印”到字符数组str中的字符数目不包含最后的结束标志符\0。也就是说调用sprintf()之后你无须再调用一次strlen()就可以知道字符数组str的长度相当于调用strlen()函数。
字符串比较函数strcmp()
strcmp是string compare的缩写即“字符串比较”。它的原型是
#includestring.h
int strcmp(const char *s1, const char *s2);
功能是比较s1和s2所指向的字符数组中的字符串返回一个int型值。s1和s2可以是字符数组名或字符指针变量名。
怎么比较呢从左往右一个字符一个字符的比较比较它们的ASCII码值。如果每个字符都相等则说明两个字符串完全相同返回0如果比较到一个不相等的字符那么后面的都不比较了。如果前面的大就返回一个大于0的值如果后面的大就返回一个小于0的值。strcmp的返回值与字符串的长短没有任何关系。比如其中一个字符串的所有字符都比较完了而另一个字符串还有字符那么系统就会默认前者小。因为当所有字符都比较完之后最后就剩下一个\0而\0的ASCII码值是最小的。