文章目录
5章(运算符和表达式)
c语言基本的运算符有 +,-,=,*和/ 指数运算用专门的c的标准数学库中有一个函数pow()提供计算 pow(3.5,3.1) 返回3.5的3.1次幂
=号是赋值 赋值行为是从右往左进行运算 =号左边必须是变量(就是应用一个存储的位子后面还有指针也就而已写这里)右边是值
例如 i = i+1 在数学的角度是难以理解的 但是在计算的的角度他解释成为 取出i变量的值然后加1在赋给新的变量i(覆盖)
赋值表达式语句的目的是把值存储到内存的位子上 , 存储值的区域叫做数据对象(data object)
左值是c语言的术语 内存中对象是指实际的存储,而左值是用于标识或者定位存储位子的标签
运算符的优先级(从高到低)
() 从左往右
+ - (一元 正负) 从右往左
* / 从左往右
+ -(二元) 从左往右
= 从右往左
6章(while for do while)
==符号是判断是否相等
while 循环
首先当while a == 1时进行以下的循环直到a不等于1就退出
例子
status = scanf(“%1d”,&num);
while (status == 1)
{
status = scanf("%1d",&num);
}
以上的代码在c语言中可代替成
while (scanf("%1d",&num) == 1);
{
//循环行为
}
scanf函数完成后会生成一个返回值然后用这个返回值去控制这个循环行为
scanf函数的返回值是根据你输入的有效的参数的个数,上面的scanf函数只给了一个%1d所以有效的值最多为1所以函数返回值最大为以,还有另外一种情况就是不输入或者输入错误的值返回是0
while循环的格式
while (expression)
statement;
以前的程序势力都是用关系表达式,也就是说expression都是值之间的比较,可以使用仍和表达式,如果expression为真(或者说非0)就执行statement一次然后再判断expression。在expression为假(0)之前,循环每一次执行的循环叫做一次迭代(iteration)
c语言中的关系运算符(看起来和bash shell里面的一样)
< 小于
<= 小于等于
== 等于
>= 大于等于
> 大于
!= 不等于
注: 浮点数尽量用< 或者> 因为浮点数的舍入误差会导致在逻辑上应该相等的2个数不相等
以上的while中提到了while循环中进入循环的条件是真就进入循环是假就退出,那么什么是真什么是假了?在c语言中最显然易见的是
#include <stdio.h>
void main(void)
{
true_z = (10 > 2);
false_z = (10 == 2);
printf("true is %d ,false is %d",true_z,false_z);
return 0;
}
返回结果是
true is 1,false is 0
表明返回一个整数真就为1假就为0 ( ()号可以用来做比较 和bash里面一样)
要写一个死循环就
while (1)
{
...
}
注意一点 while(1)中括号中的数不等于0就是真,也就是只要括号内的数字不是为0就执行循环,也就是while后面的括号中默认为1也就是真.
关系运算符的优先级比赋值运算符低比赋值运算符高
运算符 ()
|
- +(这个是正负) *(指针运算符) ++ -- sizeof ! |
* / % | 由
+ - | 高
| 到
关系运算符 < > <= >= | 低
== != |
|
赋值运算符 = ↓
for循环
#include <stdio.h>
int main(void)
{
const int NUMBER = 22;
int count;
for (count = 1;count <= NUMBER;count++)
printf("Be my valentine!\n");
return 0;
}
上面的式子首先for循环中有三项每一项由分号隔开
- 首先第一项是初始化,只是在for循环开始的时候执行一次,
- 第2项表达式是测试条件,在执行循环之前对表达式求值,当表达式为假也> 就是count>NUMBER的时候循环结束,,
- 第三个表达式是执行更新,在每一次循环结束的时候求值。
注意:
- for圆括号内的也叫控制表达式,他们都是完整表达式
- for循环非常灵活他也可以用字符表示计数器
- for循环里面的选项也可以省略例如但是分号不能省略
例如
ans = 2
for (n=3;ans<=25;)
ans = ans *n
但是如果你省略第二项那么这个循环就默认为真这个for循环就会一直执行这个循环
for (;;)
printf("-------");
也可以这样写for循环
for (printf("keep inter number!\n");number != 6;)
scanf("%d",&num);
printf("this is one i want!\n");
return 0;
上面这个式子先执行初始化也就是执行printf的语句然后num是否不等于6,等于6就为假就会退出循环,如果不等于6就为真继续执行循环
其他的赋值运算符
dimes -= 2 等价于 dimes = dimes - 2
bunnies *= 2 等价于 bunnies = bunnies * 2
time /= 2.73 等价于 time = time / 2.73
reduce %= 3 等价于 reduce = reduce % 3
x *= 3 * y + 12 等价于 x = x * (3 * y + 12)
与一般形式相比上面的代码生成机器语言更高效
逗号运算符
逗号运算符的作用是扩展for循环的灵活性,以便在循环头中包含更多的表达式。例如
#include <stdio.h>
int main(void)
{
const int FIRST_OZ = 64;
const int NEXT_OZ = 20;
int ounces,cost;
printf(" ounces cost\n");
for (ounces = 1,cost = FIRST_OZ;ounces <= 16;ounces++,cost += NEXT_OZ)
printf("%5d $%4.2f\n",ounces,cost/100.0);
return 0 ;
}
逗号运算符不光局限于for循环中使用,但是这是他最长用的地方。逗号运算符有2个其他的特性。
首先,他保证了被他分割的表达式从左往右求值,也就是在逗号左侧项所有的结果都在执行逗号右侧之前发生因此在上面ounces在cost之前初始化
这个式子
ounces++,cost = ounces *FIRST_OZ
在这个式子中首先执行的是ounces++它自身加1然后执行自身加1后的ounces *FIRST_OZ再赋值给cost
还有一个例子houseprice = 249,500;
这个例子不是错误的他中间有一个逗号编译器把他解释为houseprice = 249为一个句子后面的500为一个句子
等同于
houseprice = 249;
500;
do while
while循环和for循环都是入口条件的循环,就是在循环的每次迭代之前都检查测试条件,所以有可能根本不执行循环体中的内容。c语言还有出口条件循环,就是在每一次迭代之后检测,这保证了至少执行一次迭代的内容。这种循环叫做do while
例如
#include <stdio.h>
int main(void)
{
const int secret_code = 13;
int code_entered;
do
{
printf("to enter the triskaidekaphobia therapy club,\n");
printf("please enter the secret code number:");
scanf("%d",&code_entered);
} while (code_entered != secret_code);
printf("congratulations! you are cured1\n");
return 0;
}
do while的格式
do
statement
while (expression);
statement可以是一条简单的语句或者是复合语句。
注意do while循环以分号结尾,
但是我们怎么选择循环了?
首先我们要确定是需要入口条件循环还是需要出口条件循环,通常,入口条件循环用的比较多,有几个原因,
- 其一 一般原则在执行循环之前测试条件比较好
- 其二 测试放在循环的开头,程序的可读性更高。
- 另外 在许多的应用中,要求在一开始不满足测试条件时就直接跳过整个循环
然后,我们假设需要一个入口循环条件,是用for还是while了?这个取决与个人的爱好,因为二者皆可
其实
for (;test;)
和
while (test)
相同
数组(array)
首先我们搞清楚字符串和数组的存储方式
- 字符串
字符串都是被存入在char类型的数组中 - 数组
数组是按照顺序存储的一系列类型相同的值,例如10个char类型,15个int类型
所以数组和字符串的区别是一个char类型的数组最后一个元素是\0那么他就是字符串
整个数组有一个数组名字,通过整数下标访问数组中的单独项,或者叫做元素
注:下标是入a[0],a[5]中0和5 a表示一个数组,0和5是下标,0代表数组中第一个元素,5代表数组中第6个元素
给数组赋值
float debts[20];
debts[3]=4.33;
debts[5]=1.2e+21;
//也可以
scanf("%f",&debts[7]);
但是注意当你的赋与数组元素的值的位子超出了数组实际的元素位子,也就是按上面为例 debts数组有20个float是从debts[0]一直到debts[19]一共20 个存储空间,当我们存入的数据指向的是debts[21]这明显超出了他元素的位子,那么这个值会被存入到一个另外的空间,有可能是别的函数空间,有可能导致真个程序失败。编译器为了运行速度不会去检查下标是否正确
数组的类型可以是任意数据类型。
用于识别元素的数字被称为下标,索引或者偏移量。下标必须是整数(下标就是debts[2]中的2,下标从0开始计数,数组的元素被依次存在内存的相邻的位子)
第7章,c语言控制语句:分支和跳转
if语句
if语句的格式
if (expression)
{
statement;
}
如果expression为真(也就是1)我们就执行statement,否则为假(也就是0)就跳过statement
expression一般是关系表达式,也就是比较两个量的大小
statement是一个简单的语句
if else语句
if else语句可以再2条语句中间选择
格式
if (expression)
{
statement
}
else
{
statement2
}
当expression为真(为1)就执行statement,如果当expression为假(为0)就执行statement2语句
但是我们为什么要加花括号,因为c语言规定if和else中间只能有一条语句,多了就要使用花括号
getchar()和putchar()函数
getchar()和putchar()是###字符###输入输出函数
a = getchar();和scanf("%c",&a);一样
putchar(a); 就是printf("%c",ch);
#include <stdio.h>
int main(void)
{
char chara;
chara = getchar();
while (chara != '\n' )
{
printf("%c",chara);
chara = getchar();
}
}
这个程序很好的体现了getchar
首先getchar()让你输入一个字符,但是你输入了一个字符串,那么getchar会把你的输入都存入到缓存区中然后读取第一个字符,后面的字符等你再执行getchar()函数的时候输入,它并不会重复的让你输入,如果你输入完了后面还有getchar()他会当空直接退出。
由于这些函数他们只处理字符,所以他比通用的scanf()和printf()更快
注意getchar()和putchar()他们只处理字符所以他们没有转换说明,他们通常在stdio.h头文件中
getchar这个函数非常有意思,首先他是只能读取一个字符(他的作用就是从缓冲区读取字符,我们在屏幕上输入的字符都是存储到缓冲区中的),这个字符是存储在缓冲区中的,这些字符其实是当作整数存储的。
getchar()是从键盘中读取字符,它一次接受一个字符(ch = getchar()如果ch是char类型);如果一次敲的字符多于一个,包括回车字符,它会将剩下的字符存入缓存中,下次执行ch = getchar()的时候继续把缓冲区中剩下没有读完的字符赋予ch,而且最后\n也会赋予ch,因为getchar把\n看做输入完成的信号,并且把\n存入缓冲区中。
ctype.h头文件
isalnum() #如果是字母或者数字就返回真否则就返回假
isalpha() #如果是字母就返回真否则就返回假
isblank() #如果是标准的空白字符(空格或者换行符)就返回真否则就返回假
iscntrl() #如果是控制字符(ctrl+c) 就返回真否则就返回假
isdigit() #如果是数字就返回真否则就返回假
isgraph() #如果是除空格之外任意可打印的字符就返回真,否则就返回假
islower() #如果是小写字符就返回真否则就返回假
isuppper() #如果是大写字符就返回真否则就返回假
isprint() #如果是可打印字符就返回真否则就返回假
ispunct() #如果是标点符号(就是除了空格,字母,数字,字符以外的任何可打印字符)就返回真,否则就返回假
isspace() #如果是空白字符就返回真,否则就返回假
tolower() #如果是大写字符就返回小写字符,否则返回原始参数
toupper() #如果是小写字符就返回大写字符,否则返回源是参数
else if多重选择
if
else if
else if
else
逻辑运算符
&& — 与
例如 a < b && c < d 这个式子&&左边的a<b和&&右边的c < d都为为真,那么’a < b && c < d’ 这个式子才是真的
|| — 或
例如 a< b || c < d 这个式子||左边的a < b为真或者||右边的c < d为真,那么这个式子‘a < b || c < d’为真
! — 非
!他的运算优先级很高,和递增 递减正负一样高,就只比括号低
&& || 他们的优先级很低只比赋值运算高,比关系运算低,而且&&的优先级高于||
&&他还可以表示范围
if (range < 100 && range > 50)
编写一个统计一串字符的字符,单词,行数的程序
条件运算符 ? :
c提供条件表达式,作为if else的一种便捷方式
例如:
x = (y < 0) ? -y : y;
上面是一个三元运算符(因为由三个运算对象)他的意思为,如果 y < 0为真,那么x = -y 否则 x = y 可以写成这样
if (y < 0)
x = -y;
else
x = y;
循环辅助: continue和break
continue语句
3种循环都可以使用continue语句,当执行到continue的时候就结束本次迭代,开始下一次循环
break语句
3中循环都可以使用break,当循环执行到break的时候,他不会像continue一样结束本次迭代,然后执行下一次迭代,break他会直接的退出整个循环,然后执行循环后面的下一个语句
多重选择 switch和break
我们可以用if else if … else来进行多重选择但是大多数情况下使用switch更加简单
switch (变量)
{
case 常量1:
语句1
case 常量2:
语句2
case 常量3:
语句3
case 常量4:
语句4
default :
语句4
}
当switch后面的变量等于常量1就执行,语句1,等于常量2就执行语句2,如果都不等于就执行default后面的语句4
多重标签
字符输入输出和输入验证
单字符I/O getchar()和putchar()
以前我们提到过getchar和putchar()每一次只处理一个字符,你有可能觉得这个非常的笨拙,但是这个的确是大部分文本处理程序处理的核心方法
自从ansi c 之后c语言就把stdio.h头文件和getchar()和putchar()相关联,这就是为什么程序中要包含这个头文件的原因(其实getchar()和putchar()不是函数,他们是一个宏)
# include <stdio.h>
int main(void)
{
char ch;
while (( ch = getchar()) != '#' )
{
putchar(ch);
}
return 0 ;
}
#当getchar输入的字符等于#时就停止putchar(ch)
为何输入的字符能直接显示在屏幕上?如果用一个特殊字符(如,#)来结束输入,就无法在文本中使用这个字符,是否有更好的方法输入?要回答这些问题我们首先要了解C语言如何处理键盘输入,尤其是缓冲和标准输入文件的概念。
缓冲区的概念
什么是缓冲区
首先我们执行输入的操作的时候,我们在按下enter键之前是存储在缓冲区中的,按下enter后程序才可以使用用户输入的字符
没有缓冲区是什么样的,我们输入一个字符就直接被程序应用
我们为什么要用缓冲区?
首先,吧若干个字符作为一个快传输比,把字符逐个传输要节约时间,其次如果用户打错字可以通过键盘修复,只有最后按下enter建的时候,传输的才是正确的输入
虽然缓冲区的好处有很多,但是有些交互程序也需要无缓冲输入,例如,在游戏中你按下一个建执行响应的指令,因此无缓冲和有缓冲都有用户之地
缓冲分2类
- 完全缓冲i/o
完全缓冲io是指当缓冲区被填满的时候才刷新缓冲区,通常出现在文件输入中。缓冲区的大小取决与系统,常见的是512个字节,和4096个字节,
- 行缓冲i/o
行缓冲IO是在出现换行符的时候刷新缓冲区。键盘输入时行缓冲输入,所以在按下enter键后才刷新缓冲区。
那么我们时使用缓冲输入还是无缓冲输入?
ANSI c 和后续的c标准都是规定输入缓冲的,但是最初的k&r把这个决定权交给了编译器的编写者。
ANSI c决定把缓冲区作为标准的原因是,一些计算机不允许无缓存输入
文件和流和键盘输入
文件是存储器中存储信息的区域。通常,文件都保存在某种永久存储器中,毫无疑问文件对于计算机系统相当的重要,例如,你编写的c程序就保存在文件中,用来编译c程序的程序也保存在文件中,后者说明某些程序需要访问指定的文件。
例如,当编译存储在名字为echo.c文件中的程序时,编译器会打开echo.c文件读取其中的内容,当编译器处理完成后,会关闭改文件,其他程序,例如文字处理器,不仅要打开,读取和关闭文件,还要把数据写入文件
c语言有很多用于打开,读取,写入,关闭文件的库函数。从较低的层面上,c可以使用主机操作系统的基本文件工具直接处理文件,这些直接调用操作系统的函数称为底层io
从概念上来看c程序处理的时流,而不是直接处理文件
流(stream)是一个实际输入或者输出映射的理想化数据流,这意味着不同属性和不同种类的输入,由属性统一的流表示。于是,打开文件的过程是把流和文件相关联,而且读写都通过流来完成
文件结尾
计算机系统要以某种方式判断文件的开始和结束,检测文件结尾的一种方法是,在文件末尾放一个特殊的字符标记文件结尾,如今有一些操作系统有了更好的方法标记结尾比如用内嵌的ctrl + z
操作系统还有一种方法来确定文件的末尾,就是存储文件大小的信息(元数据)如果元数据显示文件有3000个字节当读取到3000个字节的时候就意味着到达了文件的末尾
无论操作系统使用什么方法来检测文件的结尾,在c语言中,用getchar()读取文件检测到文件结尾的时候将返回一个特殊的值,即EOF(end of file的缩写) scanf()检测到文件结尾的时候也会返回一个EOF,通常eof定义在stdio.h文件中;
通常EOF为-1,因为-1不对应getcahr()scanf()的任何返回值
#include <stdio.h>
int main(void)
{
int ch;
while ((ch = getchar()) != EOF )
{
printf("%c",ch);
}
return 0;
}
以上的程序表示getchar(),他的返回值是int,上面我们提到过将整数(纯数字没有引号)存入char类型的变量后就直接按2进制存储输出函数printf或者putchar()都可以按照数字和字符输出,数字输出就是本数字,字符输出就是按照ASCII输出字符,也就是说printf函数和putchar函数对于char类型和int类型可以相互转换的,int类型的变量可以用char类型%c输出 char类型可以用int类型输出
那么我们为什么要用int类型的数字类型了?因为ASCII没有-1对应的数字所以我们用int类型专门用来存储整形
#include <stdio.h>
//此程序用来学习用缓存区取数字并且验证,当你取的字符是Y或者以y开头的就显示输入正确,当你输入以别的开头的就会报错,里面巧妙的用了while循环加上getchar函数耗尽缓冲区后面的字符
int main(void)
{
int guess = 0;
char respose;
printf("猜字母\n");
while ((respose = getchar()) != 'y')
{
if (respose == 'n' )
{
printf("输入错误,您输入的次数为%d\n", ++guess);
}
else
printf("输入错误,请输入n或者y,您输入的次数为%d\n", ++guess);
while (getchar() != '\n') //最后一次从缓冲区取出的字符等于换行符的时候,那么就退出,但是这时最后一个换行符已经被取出来了不可能再放回缓冲区,所以缓冲区就没有东西了。
{
continue;
}
}
printf("答对了!!!");
return 0;
}
再读取缓冲区的字符时候scanf()和getchar()有很大的不同
首先getchar他会从缓冲区读取每一个字符包括换行,空格,制表符等等然后做下一个操作,但是scanf他是从缓冲区中跳过空格,制表符和换行符,然后做下一个操作比如用&计算出变量的地址然后传入进去。
下一个实例程序结合了getchar和scanf getchar用来取缓冲区第一个字符scanf用来取后面的以为他可以跳过空格
#include <stdio.h>
void display(char cr,int lines,int width);
int main(void)
{
char ch ;
int rows , cols;
printf("输入一个字符2个数字,2个数字是代表行和列,第一个输入的字符会按照这个行和列打印出来\n");
while ((ch = getchar()) != '\n')
{
scanf ("%d %d",&rows,&cols);
display(ch,rows,cols);
}
}
void display(char cr,int lines,int width)
{
int row,col;
for (row=1;row<=lines;row++)
{
for (col=1;col<=width;col++)
{
putchar(cr);
}
printf("\n");
}
}
第9章函数
复习函数
首先,什么是函数(function)?函数是完成特定任务的独立的程序代码单元。一些函数执行某些特定的动作,如printf()把数据打印到屏幕上,一些函数找出一个值为程序使用,如strlen()把字符串的长度返回给程序。一般而言,函数可以同时具备以上两种功能。
而我们为什么要使用函数?首先我们要提高代码的复用率,
创建简单函数
实例函数
#include <stdio.h>
#define NAME "张浩然"
#define ADDRESS "中国,湖北,襄阳"
#define PLACE "樊城区,丹江路,福如家园"
#define WIDTH 40
void starbar(void);
int main(void)
{
starbar();
printf("%s\n",NAME);
printf("%s\n",ADDRESS);
printf("%s\n",PLACE);
starbar();
return 0;
}
void starbar(void)
{
int count;
for (count=1;count<WIDTH;count++)
{
putchar('*');
}
printf("\n");
}
分析以上的程序
- 函数和变量一样,有多种类型,任何程序在使用函数之前都要声明该函数 的类型。因此,在main函数之前出现了ansi c风格的函数原型void starbar(void)
ansi c是美国国家标准协会定义的c标准,也就最开始的c89 然后iso
协会在继承ansi c的c89标准上定义了c 90 不久后ansi 和iso这两个组织联合定义了c99标准,而我们开始说的k&r C标准是贝尔实验室发明c语言的时候定义的标准,是最为原始的标准也可以叫做贝尔实验室c
- ansi c风格的函数定义
void starbar(void);
#第一个void代表函数的类型也就是表示函数没有返回值,第二个void(括号里面的)表示这个函数不带参数。分号表明这是在声明函数,不是定义函数。也就是说这行声明了程序将使用一个名字为starbar()没有返回值,没有传参的函数,并且告诉编译器在别处查找该函数的定义。
- 程序把starbar()函数原型置于main()函数前面。当然,也可以放在main()函数里面的声明变量处。放在那个位子都可以。
- 在main()函数中执行了下面的语句时调用了starbar()函数
starbar()
- 这是调用void类型函数的一种形式。当计算机执行到starbar()语句时,会找到该函数的定义并执行其中的内容。执行完成后,计算机返回主调函数继续执行下一行(本例子中主调函数时main()函数),更确切的说,编译器把c程序翻译成执行以上操作的机器语言代码。
- 程序中的starbar()和main()的定义形式相同。首先函数头包括函数类型,函数名字和圆括号,接着是左花括号,声明变量,函数表达式语句,左后以右花括号结束,注意函数头的后面没有分号,因为他是在告诉编译器我是在定义一个函数不是在声明或者调用一个函数原型。
- 程序把main函数和starbar函数放在一个文件中,当然,也可以把他们分别放在两个不同的文件中。把函数都放在一个文件中的单文件形式比较容易编译,而使用多个文件方便在不同的程序中使用同一个函数,如果把函数放在一个单独的文件中,要把#define和#include指令也放在该文件中
- starbar()函数中的变量count是局部变量(local variable),意思是该变量只属于starbar()函数。可以在除了starbar()函数的地方再次定义一个不同值的count变量,他们不会起冲突,他们是同名的不同变量。
- 如果把starbar函数看作成一个黑盒,那么他的行为是打印一行星号。不用给该函数提供任何的输入,因为他不用任何的参数也么有任何返回值
函数参数
例如:
void show_n_char(char ch, int num)
改行告知编译器show_n_char()使用的2个参数ch和num ch是char类型的num是int类型的这两个变量被称为形式参数,简称形参,和定义在函数中变量一样,形参是局部变量,属于该函数私有。这意味着,在其他函数中使用同名变量不会引起名称冲突。每次调用函数,就会给函数重新赋值
注意ansi c要求要在每个变量前声明其类型。也就是说,不能像普通变量声明那样使用同一类的变量列表。
void dibs(int x,y,z) //错的
void dibs(int x,int y,int z) //正确的
在定义函数原型的时候用逗号分隔的列表指明参数的变量和类型,我们也可以省略变量如
void dibs(char , int) ;
在原型中使用变量名并没有创建变量名 char仅仅代表一个char类型的变量
形参和实参
- 形参是被调函数中的变量
void dibs(char ch , int num) {} 在后面定义dibs函数的时候里面的参数ch
num 是形参
- 实参是主调函数中的参数
在main函数中调用dibs(a,10) 传入的a和10是实参
总结实参是具体的值该值要被赋给形参
函数类型和参数返回值
首先scanf函数总结 scnaf函数有一个返回值的,当你定义scanf从缓冲区取有几个字符就返回几 例如 scanf(%d %d,&a,&b) 有2个%d所以他从缓冲区取了2个数赋值给a和b这两个数时以空格隔开的所以返回2,scanf(%d %d %d,&a,&b)表明从缓冲区取了3个数但是后面只有2个值会赋给前2个然后第三个丢掉,还是返回3
scanf函数还有一个特点,当你从缓冲区取值的时候,这一次的scanf没有取完,缓冲区不会清空,而是当你在下一次执行scanf的时候继续往后取
//示例函数
# include <stdio.h>
int imin(int, int);
int main(void)
{
int evil1,evil2;
printf("enter a pair of integers (q to quit):\n");
while (scanf("%d %d",&evil1,&evil2) == 2)
{
printf("the lesser of %d and %d is %d.\n",evil1, evil2,imin(evil1,evil2));
printf("enter a pair of integers (q to quit):\n");
}
printf("bye.\n");
return 0;
}
int imin(int n ,int m)
{
int min;
if (n<m)
min = n;
else
min = m;
return min;
}
返回值min是一个局部变量,在主调函数里面主调函数不会知道他的存在的
返回值不光可以赋给变量还可以参与运算
如果我们函数声明的时候没有指定参数类类型怎么办?如 int imax();这样声明函数,在主调函数中,主调函数会把参数都存入一个叫做栈的临时存储空间里面,然后再执行某个要用到该参数的时候由这个函数从栈中读取,如果你的函数定义中有2个形式参数但是在你的栈中只存储了1个参数,那么他会在这个位置的基础上再往后读取一个值,这个参数是什么我们就是不知道了,所以带进去的数有可能非常大
还有一种可能在你的主调函数中传入的参数可能是3.0,4.0这样的float类型的数值,但是在c语言中float类型的数在当作参数传递的时候会升级称为double类型的数,所以在栈中存储float参数被升级成double类型的参数,float是存储的大小为32为 4个字节 ,double的存储大小为64位8个字节,float都被当作称为double存储了,在我们定义的函数要读取int类型的时候我们栈中存储的是double,int类型的大小是32位 4个字节2个就是8个字节,那么我们的函数从栈中读取的数据是前8个字节的数据,而我们前8个字节是第一个double的值,所以就错乱了
所以为了避免上面的错乱情况 ansi C标准,要求我们函数声明的时候要使用函数原型来声明函数的类型和返回值类型,参数的数量和每一个参数的类型。 例如
int imax(int,int);或者int imax(int a,int b)#记住这里的变量名是假名,;
递归
c语言允许函数调用自己,这种调用的过程称为递归,递归有的时候难以琢磨,有的时候很方便实现。结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。
可以使用循环的地方通常都可以使用递归,有时候用递归解决问题好,有时候用循环解决问题好,但是有的时候递归方案更简洁但是他的效率没有循环高。
演示递归
#include <stdio.h>
void up_and_down(int);
int main (void)
{
up_and_down(1);
return 0;
}
void up_and_down(int n)
{
printf("level %d :n location %p\n",n,&n);/*首先level %d代表第几层递归后面的%p代表打印内存地址对应后面的&n,&n表示内存地址 */
if (n < 4)
{
up_and_down(n+1);
}
printf("level %d :n location %p\n",n,&n);
}
上面的代码演示了一个小的递归
首先定义一个原函数up_and_down(int);
后面再主调函数main函数中向这个函数传入参数1(这个是一级递归),然后我们来看up_and_down()这个函数首先他打印了传入的数字1可以将他看作成第几级递归,然后后面打印了这个整数的内存地址&n代表内存地址而%p转换说明代表打印地址
然后一个if条件语句判断这个函数是否小于4,如果的确小于4也就是为真的话,那么我们再调用这个函数并且把传入的参数加1,然后我们到了第二级递归,打印上一级递归带入的数字+1,并且打印地址,然后又是一个判断语句以为为2再递归一次并且带入的数字+1再打印,直到递归带入的变量再下面的if语句为假的时候执行下面的printf语句,这个printf语句也是打印当前变量n的值和他的内存地址,然后了?我们到了4及递归我们这个时候返回第三级递归打印第三级递归的n的值也就是3以此类推一直返回到一级递归,下面是运行结果
但是注意一个要点,每级递归的变量N都归本级递归私有
level 1 :n location 0061FF20
level 2 :n location 0061FF00
level 3 :n location 0061FEE0
level 4 :n location 0061FEC0
level 4 :n location 0061FEC0
level 3 :n location 0061FEE0
level 2 :n location 0061FF00
level 1 :n location 0061FF20
我们看到运行结果level 1和左后level1的内存地址一样
递归的基本原理
当递归调用结束后控制权会返回到上一个级递归
每一级的递归变量都是私有的
尾递归
尾递归就是将递归调用置于函数末尾,这种形式称为尾递归。尾递归是最简单的递归形式,它相当于循环。
下面的示例代码分别用循环和尾递归计算阶乘。(阶乘:3的阶乘就是123 2的阶乘是1*2)
#include <stdio.h>
long fact(int); //使用for循环的方法
long rfact(int); //使用尾递归的方法
int main(void)
{
int num;
printf("this program calculates factorials.\n");
printf("enter a value in the range 0-12 (q to quit):\n");
while(scanf("%d",&num) == 1) //scanf函数代表按正确格式输入的数字(如果为字符不能代表正确输入)个数,如果输入的为字符不符合%d那么就返回0
{
if (num < 0)
printf("No negative numbers,please.\n");
else if (num > 12)
printf("keep input under 13.\n");
else
{
printf("loop: %d factorial = %1d\n",num,fact(num));
printf("loop: %d factorial = %1d\n",num,rfact(num));
}
printf("enter a value in the range 0-12 (q to quit):\n");
}
printf("bye.\n");
return 0;
}
long fact(int n)
{
long ans;
for (ans = 1 ; n > 1 ; n--)
{
ans *= n ;
}
return ans;
}
long rfact(int n)
{
long ans;
if (n > 0)
ans = n * rfact(n - 1);
else
ans = 1;
return ans;
}
递归的优缺点
递归既有优点也有缺点,优点是再某一些编程问题中提供了最简单的解决方案。缺点是一些递归算法会快速的消耗计算机的内存资源。另外递归不方便阅读和维护
&运算符
指针 (pointer)是c语言中最重要的概念之一,用于存储变量的地址
假设有pooh变量名,那么&pooh就是变量地址,可以把地址看成是变量存储再内存中的位子
假设 pooh=24变量pooh的存储地址是0B76(16进制),那么后面的这个语句printf("%d,%p",pooh,&pooh);这个语句会输出成24 0B76
更改主调函数中的变量
有x和y两个变量,如果我们想要交换2个变量中的值有一个小技巧,就是再加一个变量tem当临时存储
tem = x; x = y ; y = tem
指针(pointer)
从根本上看指针就是一个值为内存地址的变量(其实不严谨指针变量是一个变量用来存放指针,而指针就是一个地址)
所以我们可以创建一个指针prt
prt = &pooh #这样我们就创建了一个指针prt,首先我们用&变量pooh的地址然后把这个地址赋予变量prt
如果我们想要找出指针prt指向的值我们可以用简介运算符*
注意这个*可不是乘法的意思,他是通过指针找出他所指向的具体值,而不是内存位置
val = prt #找出prt所指向的值赋予变量val,结合上面prt = &pooh我们可以得出 val = pooh
上面我们用一个普通的变量来存储一个指针有一个问题啊普通的变量可以随意更改的,所以我们创建变量,先要声明指针变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作需要知道操作对象的大小,,而且程序必须知道存储在指定地址上的数据类型。long和float他们的大小相同都是4个字节,但是他们存储的数字却完全不同,所以我们这样声明指针
int * pi #定义一个指向整数型变量的指针
char * pc #定义一个指向字符类型变量的指针
…
…
int表明存储什么类型变量的地址,上面的表明声明的变量是一个指针
*和指针名之间的空格可有可无,通常程序员在声明指针时候使用空格,在引用变量时省略空格
使用指针在函数之间通讯
示例代码
#include <stdio.h>
void interchange(int * a,int * b);
int main()
{
int x = 5;
int y = 10;
printf("originally x = %d and y = %d.\n",x,y);
interchange(&x,&y);
printf("now x = %d and y = %d",x,y);
return 0;
}
void interchange(int * a,int * b)
{
int tem;
tem = *a;
*a = *b;
*b = tem;
}
10章数组和指针
数组
前面我们数组是由一系列相同的元素组成,需要使用数组时要告诉编译器数组中包含多少元素和元素类型,编译器根据这些信息真确的创建数组。普通变量可以使用的类型,数组也可以使用。
float candy[355] #定义一个变量candy首先他是一个数组是有355个连续的float类型组成candy[0]表示第一个元素,candy[354]是第355个元素
我们如何初始话数组?以int zhr[12]为例
首先我们有一个数组zhr他有12个相同的int元素组成他的初始化如下
int zhr[12] = {1,2,3,4,5,6,7,8,9,10,11,12};
在程序运行的过程 中有可能会改变程序中数组中的值我们可以加上const关键字来让这个数组只读。 例子如下
const int zhr[12] = {1,2,3,4,5,6,7,8,9,10,11,12};
如果我们的数组中元素的个数多于初始化时给的个数,也就是初始化的值少于元素的值那么会怎么办?
#include <stdio.h>
#define DEFAULTS 5
int main(void)
{
int zhr[DEFAULTS] = {1000,2000,3000};
int i;
printf("%2s%12s\n","i","zhr[i]");
for (i = 0;i < 5;i++)
{
printf("%2d%12d\n",i+1,zhr[i]);
}
return 0 ;
}
运行结果如下
i zhr[i]
1 1000
2 2000
3 3000
4 0
5 0
Process returned 0 (0x0) execution time : 0.210 s
Press any key to continue.
说明初始化的值的个数小于元素个数,那么只有前面几个元素会被赋值,后面没有匹配上的元素会被置于0
如果初始化的值的个数大于元素个数那么编译器直接报错。
以前我们初始化一个特定的元素非常麻烦例如c99之前的初始化第5个元素的方法
int zhr[6] = {0,0,0,0,5,0}
这样要把所有的元素写一遍非常麻烦c99规定我们可以用一种更简单的方法同样初始化第5个元素
int zhr[6] = {[4] = 5}
这样就初始化了第5个元素,为什么写[4]了?因为zhr[0]代表第一个元素zhr[5]代表第6个元素,所以[4] 意为zhr[4]代表第五个元素 但是在c99之前这样是并不被允许的 c99之前的语法是
int zhr[6] = {0,0,0,0,5,0} //一般的初始化在初始化一个元素后,未初始化的元素都会设置成0
给数组元素赋值
首先c语言不允许把数组作为一个单元赋值给另一个数组,除了初始化以外也不允许使用花括号列表形式赋值,一下列出了错误的赋值方式
#include <stdio.h>
#define SIZE 5
int main(void)
{
int oxen[SIZE] = {5,3,2,8}; //正确的
int yaks[SIZE];
yaks[SIZE] = oxen[SIZE]; //下标越界
yaks=oxen; //不能将数组作为一个单元赋值给另一个数组
yaks[SIZE] = {5,3,2,8}; //不起作用大括号只能用于初始化
}
数组边界
在使用数组的时候注意数组下标越界(比如有20个元素,下标之能是0~19,什么负数不存在)。因为大部分编译器不会检查这种错误,但是有的编译器会发出警告,然后继续编译
因为c语言新人程序员不检查边界,c程序可以运行的更快
多维数组
什么是多为数组? 多为数组就是数组的数组…,比如说有多主数组,每一个主数组中有5个元素,这就是二维数组 一维数组和二维数组一样在定义的时候是有5个元素后面就写5 是三行五列就写[3][5]但是引用元素的时候减一因为从0开始算的
float zhr[5][12] //这个二维数组中定义了5个主数组,每一个主数组有12个元素
在计算机内部他是这样存储二维数组的
zhr[0][0] zhr[0][1] zhr[0][3].....zhr[0][12]
zhr[1][0] ...zhr[1][12]
...
...
zhr[5][0] zhr[5][1]......zhr[5][12]
先存储第一个包含12个元素的数组再存储第二个包含12个元素的数组以此类推
初始化二维数组
const int zhr[5][6] =
{
{1,2,3,4,5,6},
{6,5,4,3,2,1},
{1,2,3,4,5,6},
{6,5,4,3,2,1},
{1,2,3,4,5,6}
};
指针和数组的操作
首先回顾指针的操作
int a = 5; //定义一个int类型a赋值5
int b = 6;
p = &a; //用&运算符运算出变量的地址然后赋给变量p但是不推荐
int * p; //这样是常用的创建一个int类型指针p
p = &a; // 给指针p赋值变量a的地址
* p = b //通过*运算符(这个不是乘法这个是通过指针p去寻找指针p里面存储的地址的真实地址,然后再把b的值赋给他)
这里指针和数组的相似之处是我们有一个数组marbles[5],他里面有5个元素,而marbles这个数组的变量名代表改数组的首地址 maribles和& a 有一定的相似程度所以我们也可以直接把maribles赋值给一个指针
int maribles[5];
int * p;
p = maribles; //这样直接把maribles这个数组的地址赋给指针p了
所以即能使用数组名字表示指针,也可以使用指针表示数组名
在函数原型中这样写
int sum(int maribles[], int n) //如果需要n的话就加上,int maribles表示maribles是一个指向int的指针,也可以写成int *maribles,但是第一种写法不光表示他是一种指针,还表示他是一个数组,
其实一下4中函数原型的声明方法是一样的
int sum(int maribles[],int n)
int sum(int * maribles[], int n)
int sum(int * ,int n) // 因为函数原型可以省略函数名字
int sum(int [] , int n ) // 因为函数原型可以省略函数名字
再说细一点如果maribles这是一个数组,那么maribles这个变量的等于&maribles[0] 都表示数组元素首元素的地址
往后代码都存往fedora
#include <stdio.h>
#define SIZE 4
int main(void)
{
short datas[SIZE];
short *pti;
short index;
double bills[SIZE];
double *ptf;
pti = datas;
ptf = bills;
printf("%23s %15s\n", "short","double");
for (index = 0;index < SIZE;index++)
{
printf("pointers + %d: %10p %10p\n",index,pti + index,ptf + index);
}
return 0 ;
}
以上代码首先定义一个short类型的数组和指针,还有double类型的数组和指针,然后再for里面指针+1并不代表指向下一个字节的地址而是代表下一个元素的地址,也就是代表增加一个存储单元,对于数组而言+1代表下一个元素地址,因为他是一个数组首先+0代表第一个元素的地址,既指针变量指向的是标量变量,要知道变量的类型否则*pt无法从正确的地址上取回地址
- 指针的值是它指向对象的地址,地址的表示方法依赖于计算机内部的硬件,很多计算机都是按照字节编址,意思就是内存中的每一个字节都按照顺序编号,这里一个较大的对象的地址(如double类型的变量)通常是该对象的第一个字节地址。
- 在指针前面使用*运算符可以得到该指针所指对象的值,
- 指针+1指指针的值递增他所指向类型的大小(以字节为单位)
指针表示法和数组表示法
指针表示法更接近机械语言,所以编译器在编译时能产生效率更高的代码。
指针操作
可以队指针进行那些操作?c提供了一些基本的指针操作,下面的程序中演示了8种不同的操作,为了显示每种操作的结果,该程序打印了指针的值(指针指向的地址),存储在指针指向地址上的值,以及指针自己的地址,
详情见fedora pointpos.c文件
注意指针减去指针一般用于一个数组中的不同元素,指针减去指针得到的是一个整数,意思为两个指针中间相差多少个int类型(例子种pointer是int类型)
const的使用
const的意思是定义一个read only 指针,数组,或者变量他#define差不多但是比define灵活多了
const int days[MONTHS] = {31,28,31,30,31,30,31 }
//这样定义了一个read only的数组
double rates[5] = {88.99,100.12,59.45,183.11,340.5} ;
const double * pd = rates; //把他指向数组
- pd = 29.89; //不允许
rates [0] = 99.99 //允许,因为他只对指针pd做了readonly 并没有限定数组
指针和多维数组
首先我们定义一个二维数组
zippo[4] [2];
- 首先我们在前面知道数组名字zippo是该数组元素的首地址。本例子中zippo是一个2维数组,所以zippo的值和&zippo[0]的值一样也就是他们都存储的是第一行数组第一个元素 zippo[0]里面包含2个值, 而zippo[0](把这个看成一个变量和zippo一样)的值和他的首元素的值的地址一样也就是 zippo [0] = &zippo[0][0],也就是说zippo的值是一个占用2个int大小对象的地址,zippo[0]是一个占用一个int大小对象的地址
- zippo[0] + 1和zippo +1他们的值不同,因为zippo指向的是一个占2个int字节的数,zippo存储的是他们的地址 zippo[0]指向的是一个占1个字节的数,zippo[0]存储的是他的值,所以 zippo + 1 代表增加2个int类型元素后的对象的地址,zippo[0] +1 代表增加1个int类型元素的地址
- 首先我们根据以前学的知识了解到,zippo这个变量存储的是地址也就是zippo[0]这个地址(因为前面定义二维数组的时候定义了zippo[4][2],所以zippo变量这里存储的是zippo[0]这里面的2个数组的地址),zippo[0]这个存储的是zippo[0][0]这个变量的地址,所以*(zippo[0])所取得的值是zippo[0][0]这个值也就是第一行数组中的第一个值,zippo代表了指向zippo中存储的地址zippo[0]也就是第一行数组,然后zippo中间存储的是一个地址是第一行数组中的第一个元素zippo[0][0]所以zippo取出的是一个地址也就是zippo[0]存储的值,就是第一行数组第一个元素的地址。也是说**zippo=*&zippo[0][0]=*zippo[0]
#include <stdio.h>
int main(void)
{
int zippo [4][2] = { {2,4},{6,8},{1,3},{5,7}};
printf("zippo = %p,zippo + 1 = %p\n",zippo,zippo + 1);
printf("zippo [0] = %p,zippo[0] + 1 = %p\n",zippo[0],zippo[0] + 1);
printf(" *zippo = %p,*zippo + 1 =%p\n",*zippo,*zippo + 1);
printf("zippo[0] [0] = %d,\n",zippo[0][0]);
printf("*zippo[0] = %d\n",*zippo[0]);
printf("**zippo = %d\n",**zippo);
printf("zippo[2][1] = %d\n",zippo[2][1]);
printf("*(*(zippo+2) +1)=%d",*(*(zippo+2)+1);
return 0;
}
使用指向多为数组的指针
首先我们可以定义指针pz但是我们的数组是二维数(zippo)组一旦数组传指针进来说明指针要给他准备2个地方存储地址因为zippo[0] 第一行列表中有2个数所以我们这样定义
int (* pz) [2]; //因为[]优先级高于*
然后 pz = zippo;//赋值
pz = %p pz等于zippo数组的第一行数组的2个元素而这个式子最后等于 zippo[0]中的第一个数字的地址,因为zippo的第一行元素传给了一个二维指针 打印pz就是打印第一个元素 所以pz + 1代表后一行第一个元素
*p + 1 代表第一行的第二个元素
p[0] + 1 和上面的一行同理
pz也可以直接表示数组 pz[2][1]=zippo[2][1]//都表示第三行第二个
- 数组指针,指针数组区别
int a[3][4]这个无需多说,就是一个二维数组。
int (*p)[4]就相当于int p[][4],它就是一个二维数组的指针,可以指向一个第二维度为4的二维数组。而a就是这样的数组,因而下面是合法的。
p=a;
int *p[3]是指针数组。说白了,就是定义了三个指针,分别为p[0],p[1],p[2]。可以将他们单独拿来使用。
int a1,a2,a3;
p[0]=&a1;
p[1]=&a2;
p[2]=&a3;
边长数组 (VLA)
c99新增了变长数组,允许使用变量来表示数组的维度.例如
int quarters =4;
int regions = 5;
double sales[regions] [quarters];
- 注意变长数组不是指可以修改自创建数组的大小,一旦创建了变长数组,他的大小则保持不变。这里的变是指在创建数组的时候,可以使用变量指定数组的维度
首先会我们现场见一个带有变量和变长数组的函数原型
int sum2d(int rows , int cols, int ar[rows] [cols]);//定义函数原型这样定义必需在ar数组前面定义rows和cols因为这个数组要用到他
我们在c99/c11标准上定义了函数原型可以不用带形参所以也可以这样写
int sum2d(int , int , int [][]);//必须用星号代替维度
11章:字符串和字符串函数
第4章我们简单介绍过字符串是以空字符(\0)结尾的char类型数组。因此,可以把上一章学到的数组和指针的知识应用于字符串。不过,犹豫字符串非常常用,所以C提供了许多专门用于处理字符串的函数
表示字符串i/o
//strings1.c
#include <stdio.h>
#define MAXLENGTH 81
#define MSG "I am a symbolic string constant."
int main (void)
{
char words[MAXLENGTH] = "i am a string in a array";
const char * pt1 = "something is pointing at me.";
puts("Here are some strings:");
puts(MSG);
puts(words);
puts(pt1);
words[8] = 'p';
puts(words);
return 0;
}
puts()和printf函数一样都是属于stdio.h系列的函数,但是puts()他只显示字符串,而且自动在字符串末尾加上换行符。
在程序中定于字符串
用双引号括起来的内容称为字符串字面量(string literal),也叫做字符串常量
在双引号中的字符串在变异的过程中自动在末尾加上\0字符,都作为字符存储在内存内
- 从ansic标准起,如果字符串字面量之间没有间隔,或者用空白字符分割,c将视其为串连起来的字符串字面量。例如 char greeting[50] = “Hello, and how are” “you” “today!”;
- 如果要在字符串内使用双引号必须在每一个引号前加上一个反斜线\例如
- printf("“run,spot,run!”,and how are you today!");
- 字符串常量属于静态存储类别,说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在,即使函数被多次调用。用双引号括起来的内容被视为指向该字符串存储位置的指针。类似于把数组名字作为指向该数组位置的指针。 例如 printf("%s , %p , %c\n", “we” , “are” , “space farers”);首先第一个%s说明打印we这个字符串,%p说明转换成一个地址,就是are首字幕a的存储地址, %c说明打印一个字符,结合后面的"space farers"意为取出字符串space farers的地址也就是首字幕地址和数组一样
字符串数组和初始化
我们首先先定义一个字符串数组
const char m1[40] = “little sister”;
这种形式的初始化比标准初始化简单的多:
const char m1[40] = { ‘l’ , ‘i’ , ‘t’, ‘t’, ‘l’, ‘e’, ’ ‘, ‘s’, ‘i’, ‘s’, ‘t’, ‘e’, ‘r’, ‘\0’ };
记住后面的\0如果没有后面的斜杠0他就不是一个字符串而是一个字符数组。在指定大小的时候我们要确保指定的大小要比元素多一个来存储空字符’\0’,这里我们会疑问我们还有好多的char空间没有用了,他和我们以前学到的数组初始化一样如果初始化没有出事全部空间,那么剩下的空间设置为0,这里的0值得是char字符的空字符(\0) ,而不是数字字符0
我们也可以让编译器自己确定数组的大小,就是让函数自己查找字符串的末尾\0确定字符串在何时结束。例如
- const char m2[] = “if you can t think of anything,fake it.”;
- 注意让编译器自己计算数组的大小只能在初始化阿时候用,如果你现在先创建一个数组,稍后再填充就必须要指定大小,声明数组的时候数组大小必须是可求指的整数,再c99新增变长数组之前,数组的大小都是指定的常量,包括又整形常量组成的表达式
我们还可以使用指针表示法创建字符串例如
const char * pt1 = “something is pointing at me.”;
指针和数组
指针形式和数组形式有何不同?
首先数组
- 数组形式ar1[]的字符串再计算机内存中分配为一个内含29个元素的数组每一个元素对应一个字符,还加上一个末尾的空字符’\0’每一个元素被初始化成为对应的字符,通常字符串作为可执行文件的一部分存储再数据段中。当把程序载入内存中时,也载入了程序中的字符串。字符串存储在静态存储区中,但是程序在开始运行的时候才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意此时字符串又2个副本。一个是在静态内存中的字符串面量,另一个是存储在ar1数组中的字符串。然后编译器便把数组的名字ar1识别为该数组手元素地址(&ar1[0])的别名。在数组形式中,ar1时地址常量,不能更改ar1
- ,如果更改了ar1就改变了数组的存储地址可以执行ar1 + 1 但是切记不能执行++ar1 , 因为递增运算符只能用于变量前面,不能用于常量前面,ar1时常量,如果他修改了字符串也就丢在内存中不好找了,
指针形式
- *pt1也使得编译器为字符串在静态存储区预留29个元素的空间。另外,一旦开始执行程序,他会为指针变量pt1留出一个存储位置,把字符串的首字母地址存储到指针变量中。该变量可以改变,他和数组定义的区别时pt1是地址变量可以改变,而ar1是地址常量 不可以改变
扩充
- 数据存储的空间中的数据可以被修改,这个空间称为变量,如果空间中的数据不能被修改,这个空间称为常量。地址常量就是地址不能被修改,就像一维数组中的数组名,是一个指针常量,不可被运算和不可被改变。地址变量就是地址能修改,就像一级指针,是一个指针变量,可以通过移动下标或移动指针来改变。一般在指针前加上const表示指针指向的数据不被改变例如 const char * pt1 = “123” 代表指针指向的数据readonly,但可以改变其指向例如 pt1 = “456”,而我们在char * const pt1 = “123” 这样我们定义了一个指针指向不能被修改但是这个内存里的指可以修改。
字符串输入
在我们输入字符串的时候有一个很有意思的东西
char * pt1 = “a\123bc\a”;
很显然我们输入了一个字符串,但是我们在前面学到\是转义字符\123在内存中是怎么存储的了?是\一个字节1一个字节2一个字节3一个字节还是别的了?,实际上我们在输入字符串的时候\后面的数字代表8进制123代表83就对应的ASCII是S,所以我们在这个\123这个1个字节中存储的是S的ASCII码值83的二进制这个中间又一些转换的步骤
分配空间
我们要把一个字符串读入程序,首先必须要给他预留字符串空间,然后输入函数获取该字符串,所以我们做的第一件事就是分配空间,以便于存储稍后读入的字符串。前面提到过,这意味着必须要为字符串分配足够的空间。不要指望计算机在读取字符串时顺便计算他的长度,然后再分配空间(计算机不会这样作,除非你编写一个处理这些任务的函数)假设代码如下
char *name;
scanf("%s",name);
虽然编译会通过(编译器很可能给出警告),但是在读入name时name可能会擦写掉程序中的数据或者代码,从而导致程序异常终止。因为scanf函数要把信息拷贝到指定的低智商,而此时该参数是个未初始化的指针,那么可以指向任何地方。大多数程序员都认为出现这种情况很搞笑,但仅限于评价别人的程序时。
- 补充一点scanf函数规定接受元素必须是对应的变量地址而我们不建议上面这样写的原因是name指针没有初始化,他指向的地址有可能是任意一个地址如果我们这样存入后这个地址再被别的数据写入那么他就更改了所以我们要申请一个特定的地址专门用来存放这个字符串,而我们前面写的char * mesg = “dont be curl”; 他的步骤是先开辟一个空间专门用来存储地址然后把首字幕d的地址赋给他
最简单的时申明时显示指明数组的大小;
char name [81];
gets()函数
再读取字符串时,scanf和转换说明%s只能读取一个单词,可是在程序中经常要读取一整行输入,而不仅仅是一个单词,在许多年前gets函数就用于处理这个情况,gets()函数简单易用,他读取整行输入,知道遇到换行符,进场和puts()放在一起用这个put()函数用于显示字符串,并且在末尾添加换行符。
但是我们在别的编译器上用gets函数有可能会警报,说gets不安全,因为get只有一个参数就是变量例如gets(word),他只会知道word这个字符数组的首地址他并不检查数组装不装的下缓冲区的字符行,他就一直往字符里面输入知道在缓冲区里面遇到换行符才停止,这样有可能存入的值大于数组规定的大小这样就会造成缓冲区溢出。有的人就会用ges来攻击系统所以c99九八gets的不安全放入建议中,但是到了c11直接把他踢出去了
fgets(fputs())
fgets函数可以代替gets函数他又2个参数来解决溢出问题,首先第一个参数确定他的变量存入的地方,第二个参数指明字符的最大数量,如果为n那么他就会读取n-1个或者知道读到换行符为止,第三个参数指明要读入的文件,如果你是从键盘上面读取的话就写stdin
fputs函数是fgets的配套函数 他又2个参数一个是指明要输出的参数或者字符串,第二个代表输出的文件,如果是屏幕就写stdout,但是fputs不会像puts一样自动为你在结尾加上\n
gets_s()函数
c11新增了一个函数gets_s函数他是用来替换gets函数的他又2个参数第一个输入存储的变量,第二个带输入的字符大小,并且gets_s如果读取到了一个换行符就会抛弃他而不是存储他,如果读到最大的限制还没有读到换行符首先他会把目标数组的首字符设置为空字符,读取并且丢弃然后再继续读知道读到文件结尾或者又换行符再返回空指针,接着调用依赖实现的处理函数,可能终止或者推出程序
scanf函数
scnaf函数和前面的函数比更像是获取单词数,而不是获取字符串
字符串函数
字符串提供了多个处理字符串的函数,ansi c 将他们放在string.h头文件中。其中最常用的函数又strlen(),strcat(),strcmp(),strncmp(),strcpy(),strncpy().
strlen()函数
strlen函数用于数字符串的长度
用法 strlen(字符数组)
strcat()函数
strcat函数用于拼接字符串,接受2个字符串作为参数把第二个字符串的备份附加在第一个字符串的末尾把拼接后的字符串作为第一个字符串,第二个不变
##12章内存管理
存储类别
c语言提供了不同的模型或者存储类别,在内存中存储数据。要理解这些存储类别我们要先了解一些术语。
以前我们的程序都存储在内存中,从硬件来讲,每一个值都占用一定的物理内存,c语言吧这样的一块内存称之为对象。对象可以存储一个或者多个值。一个对象可能并未存储实际的值,但是他在存储适当的值是有一定具有相应的大小,
首先程序要一种方法访问对象。这可以通过声明变量来完成;
int entity =3;
该声明创建一个名字entity的标识符,标识符是一个名称,这种情况下标识符可以指定特定特定的对象内容。标识符遵循变量命名的规则。在本例子中标识符entity是c语言指定硬件内存中的对象方式。该声明还提供了存储在对象中的值。
但是标识符并不是指定对象的唯一路径。
int * pt = &entity;
int ranks[10];
第一行声明中pt是一个标识符,他指定了一个存储的对象,表达式*pt不是标识符,因为他不是一个名称,然而他确实指定了一个对象,在这种情况下他与entuty指定的对象相同,一般而言指定对象的表达式被称为左值,
存储类别和函数
函数也可以有存储类别,可以是外部函数(默认)或者静态函数,c99新增了第三种内别内联函数,
外部函数可以被其他的文件的函数访问,
但是静态函数只能用于定义所在的文件。假设一个文件中包含了以下函数原型;
double gamma(double);
static double beta(int,int);
extern double delta(double,int);
在同一个程序中其他的文件可以调用gamma,delta但是不能调用beta因为beta只能在本文件中使用,但是别的文件可以使用delta因为他是一个外部函数,gamma为什么可以用了?因为定义函数如果前面没有加关键字声明默认未extern类型,但是要注意的是别的文件的函数的名字不能和bata和delta相同,
但是外部函数这么好我们都把函数改成外部函数不行吗?当然可以但是经验告诉我们在我们调用b函数的时候a函数可能已经偷偷的改了里面的数据,唯一例外的是const数据,因为他在初始化后就不会修改,所以我们不要担心他们意外修改
分配内存malloc()和free()
前面我们讨论存储类别有一个共同之处,在确定用那种存储类别后,根据已制定好的内存管理规则,将自动的选择其作用域和存储期,例如
float x;
char placep[]= “danceing oxen creek”;
该声明预留了100个位子每一个位子是int类型,声明还为内存提供了一个标识符,我们回忆一下静态数据在程序载入内存时分配内存,而动态变量在程序执行这一句时分配,并且在程序离开这函数时销毁。
c语言做的可不止这些。可以在程序运行时分配更多的内存。主要的工具就是malloc()函数,该函数接收一个参数;所需的内存字数。malloc()函数会找到适合的空间存储快,这样的内存时匿名的。也就是说,malloc()分配内存,但不是不会为其赋名字。然而,他确实会返回动态分配内存的首字节地址,因此可以把他赋给一个变量,并且使用指针访问这块内存。因为char表示1字节,mallic()的返回类型头长定义为指向char的指针,然而从ansi c标准开始,c使用一个新的类型,指向void的指针,该类型相当于定义通用指针,moalloc()函数可用于返回指向数组的指针,指向结构的指针等,如果malloc分配存粹失败他将返回空指针。
我们用malloc()创建一个数组
double *ptd;
ptd = (double *) malloc(30 * sizeof(double));
以上的代码为30个double类型的值请求内存空间,并设置ptd指向该位置。注意ptd被声明指向一个double类型,而不是包含30个double类型的值的快。为什么我们前面要加上一个(double *)来强制转换成为一个double 类型的地址这样我们就创建了一个数组我们也可以这样表示
ptd[1],ptd[0]这样表示,
通常,malloc()要和free()配套使用,free函数的参数就是之前malloc()返回的地址,因此动态的分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。设想malloc()和free管理这一个内存池每一次调用malloc使用内存在调用free归还内存,这样我们就形成了复用,。当然我们不能用free()释放通过其他的方式分配的内存,而且malloc()和free都在stdlib.h头文件中。
- 注意free函数位于程序的末尾,他释放了malloc函数分配的内存,例如上面的free(ptd),free释放了他,一些操作系统在程序结束的时候会自动的释放动态分配的内存,但是有一些系统他并不会,为了保险起见请使用free,不要依赖操作系统
内存泄漏
int zhr(void)
{
double *tmp = (double *) malloc(1600 * sizeof(double));
/*..........*/
}
我们忘记在后加上free释放内存到了函数zhr结束的时候作为自动变量的指针tmp也会自动的小时但是这个1600*sizeof(double)这么多的内空间并没有消失,也不可以再用,到了下一次调用这个函数的时候会再开辟另外一片空间。再zhr函数尾部加上free就可以避免这种情况
结构体和其他数据形式
设计程序的时候,最重要的步骤之一就是选择数据的表示方法,是int还是float等等,在很多情况下简单的变量和数组还是不够。为此,c提供了结构变量(structure variable),来提高你的数据表达能力,他能让你创造新的形式
声明结构体
为什么要用结构体?因为有的时候我们的基本数据结构不够我们用,如果我们要创建一个带有int float char等等结构为一体的数据类型那那么我们就用结构来(struct)表示
首先我们申明结构,描述他里面的布局。声明类似以下这样
struct book{ //声明一个结构体book
char title[10] //声明一个字符串变量
char author[8] //声明一个字符串变量
float value; //声明一个float类型的变量
}; //结束声明
上面的代码表明了book结构是由2个char类型和1个float类型的数据类型组成 ,该声明并没有创建实际的数据对象
我们使用book结构体
struct book library; //这样把library声明为一个使用book结构布局的变量。
结构声明中用花括号括起来的是结构成员列表。
###定义结构变量
结构由2层含义。一层含义是“结构布局”,刚才已经讨论过,结构布局告诉编译器如何表示数据,但是他并未让编译器为数据分配空间。下一步是结构变量,既是结构的另一层含义。程序中创建结构变量的一行是;
struct book library;
编译器执行这个代码创建了一个结构变量library。编译器使用book模板为该变量分配空间;在结构变量声明中struct book所起的作用相当于一般声明中的int或者float,例如可以定义2个struct book类型的变量,或者甚至是指向struct book类型结构的指针;
struct book doyle, panshin, *ptbook;
结构变量doyle和panshin中都包含title,author和value部分。指针ptbook可以指向doyle,panshin或任何其他book类型的结构变量。从本质上看,book结构声明创建了一个名为struct book的新类型。
就计算机而下面声明
struct book library;
就是一下代码的简化;
struct book {
char title[10];
char author[8];
float value;
} library;
换而言之,之前结构的过程和定义结构变量的过程可以组合成一个步骤
struct {
char title[10];
char author[10];
float value;
float value;
} library;
初始化结构体
初始化变量和数组如下;
int count = 0;
int fibo[7] = {0,1,1,2,3,5,8};
结构体也可以这样初始化?是的。可以。初始化一个结构变量(ansi c 之前,不能自动初始化结构; ansi之后可以使用任意的存储类别)与初始化数组的方法相似;
struct book library = {
"the pious pirate and the devious damsel",
"renee vivotte",
1.95
};
简而言之,我们使用在一对花括号括起来的初始化泪飙进行促使话,各个初始化项用逗号分割。title成员可以被初始化成为一个字符串,value成员也可以被初始化成为一个数字
访问成员结构
结构体类似于一个超级数组
这二个超级数组中,可以是一个元素为char类型,下一个元素为float类型,再下一个为int类型的数组,数组可以通过数组下标单独访问数组,那么如何访问结构体中的成员了?使用结构成员运算符 . 即可访问结构体中的成员,例如
library.value及访问library的value部分。可以项使用任何float类型变量那样使用library.value
本质上.title .author和.value的作用相当于book结构的下标。
注意虽然library是一个结构,但是library.value是一个float类型的变量我们可以项使用其他普通的float类型的变量一样使用他
结构初始化器
c99和c11为结构提供了指定的初始化器,其语法于数组的指定初始化器类似。但是,结构的指定初始化器使用点运算和成员名(而不是方括号和下标)表示特定的元素。例如,只初始化book结构的value成员,可以这样做
struct book surprise = {.value = 10.99};
也可以按任意的顺序指定初始化器
struct book qift = { .value = 25.99,
.author = "James Broadfool",
.title = "rue for the toad"};
考虑一下的程序,
struct book qift = { .value = 18.90,
.author = "philionna pestle",
0.25};
首先我们给value成员的值是18.90但是最后我们没有用初始化器默认赋给的第三个成员也是value,但是value的值到底是多少了?其实是0.25 ,以最后一次赋值为准
结构数组
接下来我们设计一个程序程序的内容是处理多本书,每本书有着他的基本信息,上面我们由一个book结构变量来表示。为了描述2本书我们用2个结构一次类推
#include <stdio.h>
#include <string.h>
char * s_gets (char * st, int n);
#define MAXTITL 40
#define MAXAUTL 40
#define MAXBKS 100
struct book {
char title [MAXTITL];
char author [MAXAUTL];
float value;
};
int main (void)
{
struct book library [MAXBKS];
int count = 0;
int index;
printf ("please enter the book title.\n");
printf ("press [enter] at the start of a line to stop.\n");
while (count < MAXBKS && s_gets (library[count].title, MAXTITL) != NULL && library[count].title[0] != '\0')
{
printf("now enter the author.\n");
s_gets (library[count].author, MAXAUTL);
scanf("%f",&library[count++].value);
continue;
if (count < MAXBKS)
printf("Enter the next title.\n");
}
if (count > 0)
{
printf("here is the list of your books:\n");
for (index = 0; index < count; index++)
printf("%s by %s: $%.2f\n",library[index].title,library[index].author, library[index].value);
}
else
printf("No books? too bad.\n");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st,n,stdin);
if (ret_val)
{
find = strchr(st, '\n');
if (find)
*find = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
~
~
声明结构数组
申明结构数组和声明其他类型的数组相似,下面是一个声明结构数组的例子
struct book library[MAXBKS];
以上代码把library声明为一个内含有MAXBKS个元素的数组,数组声明的每一个元素都是一个book类型的结构。因此,library[0]是第一个book类型的结构变量,library[1]是第二个以此类推,因此数组名library本身并不是一个结构体他是一个数组名,该数组中的每一个元素都是struct book类型的结构变量
标识结构数组的成员
为了表示结构数组的成员,可以采用访问单独结构的规则: 在结构后面加一个点运算符,再在点运算符后面写上成员名字。
library[0].value; //第一个元素结构中的value成员
library[4].title; //第二个元素结构的title成员
注意library[2]是结构变量的名字
顺带一提 library[2].title[4]代表什么?
这是第三个结构变量中title成员字符串的第5个字符
总结一下
library //一个book结构的数组
library[2] //代表第二个元素,这个元素是book结构
library[2].title //代表这时第二个book结构类型的元素中的title成员
library[2].title[4] //表示第三个book类型元素中title成员的第5个字符
程序讨论
while (count < MAXBKS && s_gets (library[count].title, MAXTITL) != NULL && library[count].title[0] != '\0')
biaodashi s_gets(library[count].title,MAXTITL)读取;读取一个字符串为书名如果s_gets尝试读取到文件结尾后面,该表达式则返回NULL。表达式library[count].title[0] != '\0’判断首字母是否为空(既该字符串为空)s_gets看后面定义的函数
while (getchar() != '\n')
continue;
前面介绍过这样写是为了弥补scanf的不足的问题,因为scanf一遇到换行和空格就结束读取,当用户输入价格的时候可能输入一下信息:
12.50[enter] //这个enter就是回车
器传送的字符如下
12.50\n
scanf函数接受1,2,3,4,5和0,但是把\n留在输入队列中。如果没有以上的2行清理输入行的代码,就会把留在输入队列中的换行符当作空行读入,程序会意为用户发送了停止的信号,我们插入这2行代码只会在输入队列中查找并且删除\n,不会处理其他的字符,这样s_gets()就可以重新开始下一次输入
嵌套结构
有的时候一个结构包含另外一个结构的时候很方便
指向结构的指针
为什么我们要用指针指向数组?至少由4个理由可以解释,
第一就像指针指向数组的指针比比数组本身容易操控,指向结构的指针通常比结构本身跟好操控。
第二在一些早期的c实现中结构不能作为参数传递给函数,但是可以传递指针来完成
第三,即使能传递一个结构,传递指针通常更有效率
第四,一些表示数据结构中包含指向其他结构的指针。
示例程序
/* friends */
#include <stdio.h>
#define LEN 20
struct names {
char first [LEN];
char last [LEN];
};
struct guy{
struct names handle;
char favfood[LEN];
char job[LEN];
float income;
};
int main (void)
{
struct guy fellow[2] = {
{
{ "ewen", "villard"},
"grilled salmon",
"personality coach",
68112.00
},
{
{"rodney", "swillbelly"},
"tripe",
"tabloid editor",
432400.00
}
};
struct guy * him;
printf("address #1: %p #2: %p\n", &fellow[0], &fellow[1]);
him = &fellow[0];
printf("pointer #1: %p #2: %p\n",him, him + 1);
printf("him->income is $%.2f: (*hime).income is $%.2f\n",him->income, (*him).income);
him++;
printf("him->favfood is %s: him->handle.last is %s\n",him->favfood, him->handle.last);
return 0;
}
声明结构指针和初始化结构指针
声明结构指针很简单
struct guy * him;
该申明并没有创建一个新的结构,但是指针him现在可以指向任意现有的guy类型的结构。例如,如果barney是一个guy类型的结构变量,可以这样写;
him = &barney;
和数组不一样的是结构变量名字并不是结构的地址,因此在结构变量前面加上&运算符。
在本例子中fellow是一个结构数据,这意味着fellow[0]是一个结构。所以要让him指向fellow[0]可以这样写
him = &fellow[0] //不能写成him = fellow[0]因为fellow是guy结构类型,他的意思是指向这个结构元素的第一个元素,但是他的第一个元素还是结构而且结构名字不能和数组一样代表第一个变量的地址
上面的程序him指向fellow[0],him + 1指向fellow[1],注意him加上一表示him指向的地址加上84(10进制)
由上面的运行结果我们就可以看出
第一个结构数组的元素地址
0x7ffed2cf3740
第二个结构数组的元素的地址
0x7ffed2cf3794
3740-3794=54(16进制)=84(10进制)
- 补充为什么我们的内存地址是16进制乐?因为如果内存地址用2仅仅只的话太复杂了,如果我们用10进制的话又脱离了0和1的本质所以折中我们用16进制
- 16进制地址占存储空间吗?当然占空间但是他不占内存空间而占寄存器的空间cpu中寄存器里,内存地址只是一种抽象,并不是正真的物理地址,而是逻辑地址,由逻辑地址寻找到物理地址的过程是 逻辑地址–> 线性地址–>物理地址这些过程都是由寄存器完成的
因为每一个guy结构需要84个字节names.first占了20个字节 name.last占了20个字节 favfood占用了20个字节job占用了20个字节income占用了4个字节(假设这个系统的float是4个字节),顺带一提在有一些的系统中一个结构的大小可能大于他各个成员大小的和,这个是因为系统中队数据进行校准的过程中产生了一些"缝隙"。例如,有一些系统都把每一个成员都放在偶数地址上,或4的倍数的地址上,在这种系统中,结构的内部就存在未使用的缝隙
使用指针访问成员
首先我们用最通俗的方法访问 ->运算符
如果him == &barney, 那么him->income相当于barney.income
如果him == &fellow[0],那么him->income即是fellow[0].income
千万不要写成him.income因为him是指针变量不是结构
我们着重理解him是一个指针,但是him->income是该指针所指向结构的一个成员
第二种方法
因为him == &fellow[0];
那么*him == fellow[0];
因此
fellow[0].income == (*him).income
必须要使用圆括号因为.运算符的等级比星号寻址运算符等级高
向程序传递结构的信息
函数的参数把值传给函数,每一个值都是一个数字,可能是int型,float型,可能是ascii字符码,或者是一个地址,然而,一个结构比一个单独的值更复杂,所以以前的c实现不允许把结构作为参数传给函数。当前的实现已经移除了这个限制,ansi c允许把结构作为参数使用。所以程序员可以选择是传递结构本身还是传递指向结构的指针。如果你只关心结构中的一部分,也可以把结构的成员作为参数。我们接下来分析这3中传递方式
传递结构成员
只要把结构成员是一个具有单个值的数据类型既int及其相关类型,char,float,double或指针,便可以把他作为参数传递给接受该特定类型的函数。 示例代码
/*fund1.c*/
#include <stdio.h>
#define FUNDLEN 50
struct funds{
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(double, double);
int main(void)
{
struct funds stan = {
"Garlic-Melon Bank",
4032.27,
"Luck`s saveings and loan",
8543.94
};
printf("stan has a total of $%.2f.\n",sum(stan.bankfund,stan.savefund) );
return 0;
}
double sum(double x, double y)
{
return(x + y);
}
注意我们定义的sum函数及不知道也不关心实际的参数是否是结构的成员,他只要求传入的数据是double类型。
当然,如果需要在被调函数中修改主调函数的成员值,就要传递成员的地址
modify(&stan.bankfund);
这是一个更改银行账户的函数。
把结构的信息告诉函数的第二种方法是让被调函数知道自己在处理一个结构。
传递结构的地址
我们把结构体作为参数试一试看
/*fund2.c*/
#include <stdio.h>
#define FUNDLEN 50
struct funds{
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(struct funds *);
int main(void)
{
struct funds stan = {
"Garlic-Melon Bank",
4032.27,
"Luck`s saveings and loan",
8543.94
};
printf("stan has a total of $%.2f.\n",sum(&stan) );
return 0;
}
double sum(struct funds * x)
{
return(x->bankfund + x->savefund);
}
sum()函数使用指向funds结构的指针x作为他的参数。把函数地址&stan传递给该函数,是的指针money指向结构变量stan。然后通过->运算符获取 stan.bankfund和stan.savefund的值相加返回
传递结构
/*fund3.c*/
#include <stdio.h>
#define FUNDLEN 50
struct funds{
char bank[FUNDLEN];
double bankfund;
char save[FUNDLEN];
double savefund;
};
double sum(struct funds moolah);
int main(void)
{
struct funds stan = {
"Garlic-Melon Bank",
4032.27,
"Luck`s saveings and loan",
8543.94
};
printf("stan has a total of $%.2f.\n",sum(stan) );
return 0;
}
double sum(struct funds moolah)
{
return(moolah.bankfund + moolah.savefund);
当我们调用sum函数的时候表一起根据funds模板创建了一个名为moolah的自动结构变量。然后该结构的各个成员被初始化位stan结构变量相应成员的值的副本。因此程序使用原来结构的副本进行计算,
其他结构的特性
现在c允许把一个结构赋给另外一个结构,但是数组不能这样做就是说n_data和o_data都是相同类型的结构可以这样做
o_data = n_data; //把一个结构赋给另外一个结构
这条语句把n_data的每一个成员值都赋给o_data的相应成员。即使成员是数组,也能完成赋值也可以这样初始化
struct names right_field = {“Ruthie”, “george”};
struct names captain = right_field; //把一个结构初始化为另外一个结构
现在的c不光可以把结构本身作为参数传递,还能把结构作为返回值返回。
结构和结构指针的选择
假设我们要协议个与结构相关的函数,用的是结构指针作为参数,还是用结构作为参数和返回值,两者各有优缺点
把指针作为参数有2个优点,无论是以前还是现在的c语言实现都可以用这种方法,而且执行起来很快,只需要传递一个地址,缺点是无法保护数据。在被调函数中的某些操作,可能意外影响原来结构中的数据,但是到了ansi c后有了const限定符后解决了这个问题,但是把结构作为参数传递的优点是传递的数据是副本,这保护了原数据,但是传递结构的缺点是耗内存比较大
结构中的字符数组和字符指针
我们考虑下面2个结构
#define LEN 20
struct name {
char first[LEN];
char last[LEN];
};
struct pname{
char * first;
char * last;
}
上面的代码没有问题我们思考一下字符串被存在哪里,首先第一个结构内部的字符串存储在结构name中因为他在这个结构里面有个一字符数组
第二个结构的字符串存储在存储常量的区域,因为结构里面是2个指针变量,存储的是地址struct pname部位字符串分配空间但是struct name会
结合上面的结构考虑下面的赋值
struct name accountantl;
struct pname attorney;
puts("enter the last name of your accountant:");
scanf(""%s),accountant.last;
puts("enter the last name of your attorney:");
scanf("%s",attorney.last);
上面的代码给accountant赋值没有毛病,付给了结构里面的字符数组,但是我们想一下我们赋值给第二个pname结构类型的attorney变量中阿last成员字符串传到哪里了
由于attorney.last是未经过初始化的变量他的地址可以是任何值
- 这里我们也许会好奇为啥char * pt = “aaaaa”;这个可以了?因为"aaaa"表示第一个a的地址,而"aaaaaa"在程序读入的时候已经存入了内存中专门用来存储常量的区域这样我们直接将他的地址赋给指针变量即可,但是我们上面的式子了?他输入的字符串在缓冲区,我们从缓冲区取出字符串后把他放到我们指定的attorney.last这个指针变量指向的指针中(因为scanf的特性,后面的值必须是一个地址),但是attorney.last指向的是一个未初始化的地址,表示任意地址,如果这个地址的位子不够大,比如1位后面就是其他的变量,这样容易造成程序奔溃,
所以这个程序有可能崩溃,所以在这里我们还是最好用字符数组来标识
结构,指针和malloc()
如果使用malloc分配内存并且使用指针存储该内存,那么结构中使用指针处理字符串就比较合理。这种方法的优点是可以请求malloc位字符串分配合适的存储空间。可以要求用4字节存储joe和用18字节存储"rasolofomasoandro"用这种方法改写以上的程序并不难
数据结构
学习计算机语言我们要首先熟练的使用这个工具然后解决各种问题,如果想到更高的层次,工具时次要的,正真重要的是设计和创建一个项目。
研究数据表示
我们先从数据开始。假设要创建一个地址簿程序。应该使用什么数据形式存储信息?由于存储的每一页都包含多种信息,用结构来表示每一项很合适。如何表示多个项了?是否用标准的结构数据?还是动态的数组?还是一些其他形式?各项是否按照字母顺序排列?是否要按照邮政编码查找各项?需要执行的行为将如何影响存储信息?简而言之,再开始编程之前,要再程序设计上做很多决定 比如我们设计一个程序,让用户输入一年之内看过的所有电影,要存储每一部影片的信息比如片名,发行年份,导演,主演,片场,影片的种类,评级等,建议使用一个结构存储每一步电影,一个数组存储一年看过的电影