第1章开发环境搭建及调试窗口设置
1.1开发环境搭建
1.2新建项目
1.3新建代码及编译运行
#include<stdio.h>#include<stdlib.h>int main(){int i=10;printf("hello world \n");system("pause");}
1.4程序的编译过程
1.5断点及调试窗口设置
窗口设置
行号
调试
调用堆栈、内存、监视
F10 逐过程调试
微软会把所有变量的空间初始化为四个字节的cc cc cc cc(-858993460)
编译错误:能够双击定位
链接错误:看对应的函数符号
第⒉章数据类型、运算符与表达式
2.1数据类型
基本类型:整型int、字符型char、实型(浮点型)单精度float、实型双精度double
构造类型:数组类型[]、结构类型struct、联合类型union、枚举类型enum
指针类型:*
空类型:void
2.2常量
在程序运行过程中,其值不能被改变的量称为常量。
常量不同类型:整型、实型、字符型’a’、字符串”a”。
注:单引号里面没有字符什么也不是,双引号里面没有字符表示空串。
2.3变量
变量代表内存中具有特定属性的一个存储单元,它用来存放数据,这就是变量的值,在程序运行期间这些值在程序的执行过程中是可以改变的。
变量名实际上是一个以一个名字对应代表一个地址,在对程序编译连接时由编译系统给每一个变量名分配对应的内存地址。从变量中取值,实际上是通过变量名找到相应的内存地址,从该存储单元中读取数据。如下图:
可执行程序(.exe)->进程
进程地址空间:代码段、栈(空间 变量存储区域)
变量命名:字母、数字、下划线(不能以数字开头)
匈牙利命名法(使用得不多):不同类型变量以不同字母开头,英文名之间用下划线
驼峰命名法:不打下划线
大驼峰命名法:不打下划线首字母大写
2.4整型数据
2.4.1符号常量
1个解决方案在同一时间只能运行1个项目(.exe)
加粗表示其为启动项目 
右键设置


等号(=)的左边表示左操作数
等号(=)的右边表示右操作数
左值表示可以修改的值。
预处理/编译
预处理不是编译,预处理不检查编译错误,约等于文件展开、替换
预处理后
右键项目-》属性-》配置属性-》c/c++-》预处理器-》预处理到文件
(注:调成这一步后不会继续往下执行生成.exe,重新生成会失败,记得重新调回来)
预处理后的文件.i
展开后有一万多行(stdio.h只有七百行,stdio.h还含有其他头文件)
解决方法:加括号保证优先级
2.4.2整型常量的不同进制表示
#include<stdio.h>#include<stdlib.h>//预编译//不同进制赋值#define SUN (4+3)int main(){int i=SUN*2;i=0x127b;//十六进制i=0173;//八进制printf("%d\n",i);system("pause");}
计算机中只能存储二进制,也就是0和1,对应的物理硬件上是高低电频。为了更方便观察内存中的二进制情况,除了正常我们使用的十进制外,还提供了十六进制,八进制,下面我们来看一下不同进制的对应关系,首先计算机中1个字节=8位,1位即二进制的1位,存储0或者1。
int型,大小为4个字节,即32位
二进制数
0100 1100 0011 0001 0101 0110 1111 1110
最低位是2的零次方,一直到最高位为2的30次方,最高位为符号位,具体到补码部分讲解。
八进制数
011414253376以0开头标示,变化范围从0-7,对应2进制为每3位二进制变换为1位八进制,将上面的二进制每三位隔开,效果如下:
01 001 100 001 100 010 101 011 011 111 110,然后每三位对应0-7进行对应转换,就会得到八进制数011414253376,因为实际编程时,识别八进制前面需要加0,所以我在前面加了一个0。
十进制数
1278301950 直接赋值即可
十六进制
0x4C3156FE
以0x开头标示,变为范围为0-9,A-F,A代表10,F
代表15,对应2进制为每4位二进制转换为1位16进制,具体可以自行对应
首先我们新建项目整型的进制转换,如何在已有的解决方案内新建项目,如图2.4.2-1所示,在解决方案位置,右键点击,找到添加,选择新建项目,填写名字整型的进制转换,同时如图2.4.2-2所示,设置“整型的进制转换”为启动项目。
CPU
英特尔 小端:低位在前 高位在后
(char类型不存在大小端问题 整型、浮点型才有大小端问题)
小技巧:
针对我们手动转换一个数后,不知道对应的进制是否正确,可以在 windows 的“开始”菜单的“附件”命令中打开计算机软件,点击查看菜单中的程序员,得到如下图所示的计算器,通过输入一个十进制数以后,点击十六进制,八进制或者二进制,即可得到对应进制的转换结果。
二进制和十进制的转换

数位
十转二
短除法 倒着写
小数二转十
二进制 十进制
小数十转二(乘法)
十进制 二进制

(乘以2时不考虑小数点前的数字)
2.4.3补码的作用
计算机的CPU是无法做减法操作的,只能做加法,其实CPU中有一个逻辑单元叫加法器,计算机所做的减法,乘法,除法,都是由科学家将其变化为加法。那么减法是如何实现的呢,其实2-5,实际所使用的方法是2+(-5)进行的,而计算机只能存储0和1。
计算机是如何表示-5的呢,5的二进制为101,称为原码,计算机用补码表示-5,补码是对原码取反加1,也就是计算机表示-5是对5(101)的二进制进行取反加1,-5在内存中的存储是0x ff ff ff fb,因为5取反得到的是0x ff ff ff fa,然后加1就是0x ff ff ff fb,然后对其加2,最终得到的结果为0x ff ff ff fd,当最高位为1时,即代表负数,这个时候我们要得到其原码,才能知道0x ff ff ff fd的值,就是对其取反加1(当然你可以减一取反,效果是一样的),就得到3,所以其值为-3。
计算机组成原理:加法器、移位器、译码器
0000 0000 0000 0000 0000 0000 0000 0010 2
0000 0000 0000 0000 0000 0000 0000 0101 5
1111 1111 1111 1111 1111 1111 1111 1011 -5
1111 1111 1111 1111 1111 1111 1111 1101 -3
(最高位是1 则为负数)
2.4.4整型变量
有符号基本整型(signed)int
有符号短整型(signed)short (int)
有符号长整型(signed) long (int)
无符号基本整型unsigned int
无符号短整型unsigned short (int)
无符号长整型unsigned long (int)
注意:括号表示其中的内容是可选的
整型变量分为六种类型,具体对应不同类型变量可以表示的整型数范围如表所示,如果超出数据范围就会发生溢出现象,导致计算出错。
(微软为防止访问越界,两个变量栈空间有定义8个字节的保护空间,linux没有,1个字节访问越界也极容易造成程序崩溃)
1000 0000 1111 1011 80fb
0111 1111 0000 0101 -80fb
32位程序long为4字节long long为8字节 64位程序long和long long都为8字节
231=21亿
0111 1111 1111 1111 32767
1000 0000 0000 0000 -32768
#include<stdio.h>#include<stdlib.h>int main(){short a,b;a=32767;b=a+1;printf("b=%d\n",b);system("pause");}
2.5浮点型数据
2.5.1浮点型常量
浮点型常量的表示方法,有两种形式,如下表所示,e代表10的幂次,可正可负
2.5.2浮点型变量
#include<stdio.h>#include<stdlib.h>int main(){float f=1.456;if (1.456==f){printf("f is equal to 1.456\n");}else{printf("f is not equal to 1.456\n");}printf("%f\n",f);system("pause");}
我们通过float关键字或double关键字进行浮点型变量定义, float类型占据4个字节大小的内存空间,double占据8个字节的空间,与整型数据的存储方式不同,浮点型数据是按照指数形式存储的。系统把一个浮点型数据分成小数部分和指数部分,分别存放。指数部分采用规范化的指数形式,指数也有符号位。
(指数部分是指存储的幂次:28=128 2128=3.4*1038)
(小数部分:223=8388608 只能表述6位数的最大数,以及8开头的7位数)
数符占用1位,是0就代表正数,1就代表负数,下面我们用浮点数4.5举例来讲一下IEEE-754存储标准:
格式 SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM
二进制 01000000 10010000 00000000 00000000
16进制 40 90 00 00
S:为0,是个正数。
E:为10000001转为10进制为129,129-127=2,即实际指数部分为2。
M:为0010 0000 0000 0000 0000 000。这里,在底数左边省略存储了一个1,使用实际底数表示为1.00100000000000000000000
计算机并不能够计算10的幂次,指数值为2,代表2的2次幂,因此将1.001向左移动2位即可,也就是100.1,然后转换为10进制,整数部分就是4,然后小数部分是2-1刚好等于0.5,因此为4.5。浮点数的小数部分,是通过2-1+2-2+2-3等去近似一个小数的。
(王者荣耀在游戏开发中为了提高小数运算的性能:使用分数存储数据)
浮点型变量分为单精度(float型)、双精度( double型)和长双精度型(long double)三类形式。如下表所示,因为浮点数使用的是指数表示法,所以我们不用担心数的范围,以及去看浮点数的内存(自己算起来麻烦),我们需要关注的是浮点数的精度问题
我们赋给a的值为1.23456789e10,加20后,应该得到的值为1.234567892e10,但是却是1.23456788e10,变的更小了,我们把这种现象称为精度丢失,原因就是float 能够表示的有效数字为7位,最多只保证1.234567e10的正确性,如果要达到正确,我们需要把a和b均改为double类型。
#include<stdio.h>#include<stdlib.h>int main(){double a=1.23456789e10,b;b=a+20;printf("a=%f,b=%f\n",a,b);system("pause");}
2.6字符型数据
2.6.1字符型常量
用单引号包含的一个字符是字符型常量,且只能包含一个字符
以“\”开头的特殊字符称为转义字符,转义字符用来表示回车,退格等功能键。运行查看打印效果
\r 光标回到行首
\r 光标回到退行
#include<stdio.h>#include<stdlib.h>int main(){char d,c='c';//占用1个字节char e='\t';//横向跳格,显示4个空格char f='\b';char h='\\';char i='\0';//字符串的结束符,空字符d=c-32;printf("\\\\\n");printf("abc\rd\n");//r回到行首,所以得到dbcprintf("\123\n") ;//转义8进制,变为10进制是83,打印的是字符Sprintf("\x40\n");//转义16进制,变为10进制是64,打印的是字符@printf("c=%c\n",d);//得到大写字母Cprintf ("c=%d\n",c);//以10进制形式输出字符的ASCII值printf("e=%chaha\n",e);printf("abc%cd\n",f); //\b是向前退一格,得到abdprintf("%c\\n\n",h);//要输出一个\,需要转义printf("i=%cb\n",i);//空字符什么都没有system("pause");}

2.6.2字符数据在内存中的存储形式及其使用方法
字符型变量使用关键字char进行定义,占用1个字节大小的空间,一个字符常量存放到一个字符变量中,实际上并不是把该字符的字型放到内存中去,而是将该字符的相应的ASCII码值放到存储单元中,每一个字符的ASCII码值表详见附录1,当我们打印字符变量时,如果以字符形式打印,实际过程中拿字符变量的值去ASCII码表中查,查到对应的字符后,显示对应的字符。这样使字符型数据和整型数据之间可以通用,一个字符数据既可以以字符形式输出,也可以以整数形式输出。也可以通过运算获取想要的各种字符效果。
2.7字符串常量
字符串常量是一对双撇号(双引号)括起来的字符序列,合法的字符串常量:“How do you do.”,“CHINA”,“a” , “$123.45”,可以输出一个字符串,如printf(“How do you do.”);‘a’是字符常量,”a”是字符串常量,二者不同。假如我们定义字符变量c,例如 char c;如果 c=”a”;c=”CHINA”,这样的赋值都是非法的,不可以将字符串常量赋值给字符变量,c语言没有定义字符串变量的关键字,具体怎么存字符串,我们到字符数组会进行讲解。
C规定:在每一个字符串常量的结尾加一个“字符串结束标志”,以便系统据此判断字符串是否结束。C规定以字符’\0’作为字符串结束标志。
如果有一个字符串常量”CHINA’,实际上在内存中存储效果如图2.7-1,它占内存单元不是5个字符,而是6个字符,即6个字节大小,最后一个字符为’\0’。但在输出时不输出’\0’,因为’\0’是显示不出的。
2.8混合运算
字符型(char)、整型(包括int,short,long)、浮点型(包括float,double)可以混合运算。在进行运算时,不同类型的数据要先转换成同一类型,然后进行运算。
从短字节到长字节,这种类型转换是由系统自动进行的。同时编译时不会警告,如果反向进行,编译时编译器会有警告提醒。
2.8.1数值按int运算
C的整型算术运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升( integral Promotion)。
例如,在下面表达式的求值中
char a, b, c;
a=b+c;
b和c的值被提升为普通整型,然后再执行加法运算。加法运算的结果将被截短,然后再存储于a中。这个例子的结果和使用8位算术的结果是一样的。但在下面这个例子中,它的结果就不再相同。这个例子用于计算一系列字符的简单检验和。
c=(~a^b<<1)>>1;
由于存在求补和左移操作,所以8位的精度是不够的。标准要求进行完整的整型求值,所以对于这类表达式的结果,不会存在歧义性。
//数值按4个字节运算int main(){char a, b, c;a=0x0;b=0x93;c=(~a~b<<1)>>1;printf("%d\n", c);//最终c的值为108,但是如果a和b在运算过程中,你把它看成一个字节得到的是负值system("pause");}
微软masm编译器
(一个字节的情况下) (char实际按四个字节运算)
1001 0011 0x93 ff ff ff 1001 0011 0x93
左移一位(高位丢弃 低位补零)
0010 0110 0x26 ff ff ff 0010 0111 0x26
右移一位
0001 0011 0x13 ff ff ff 1001 0011 0x93
将一个字节补为四个字节运算时,前面的补位取决于本身的最高位

任何一个数的运算都是按4个字节运算的
另外一种场景是我们将两个整型常量做乘法,赋值给一个长整型时,对于编译器来讲是按照int类型进行的,例子如下,打印结果为零,无论是在VS中新建win32控制台应用程序,在32位下执行,还是在Linux下将其编译为64位的可执行程序,执行结果均为0

2.8.2 浮点常量默认按double类型运算
2.8.3类型强制转换场景
对于整型数进行除法运算时,如果除后为小数,存入浮点数,一定要进行强制类型转换
11行会因为将一个整型表达式赋值给浮点数有 warning,有丢失精度的可能。
13行会因为没有把变量l强制转换为short再做加法有warning(在java中会直接报错),如果增加了short强转,就可以去除13行的警告。
2.9常用的数据输入/输出函数
程序可以输入数据,程序处理后,会给我们一个输出,C语言通过函数库,读取标准输入,然后通过对应函数结果打印到屏幕上,前面我们学习了printf 函数,理解通过printf 函数可以将结果输出到控制台窗口。下面我们将详细讲解标准输入读取接口 scanf,getchar,以及打印到屏幕上的标准输出接口 printf,putchar。
2.9.1 scanf原理
C语言没有提供输入输出关键字(c++有提供),C语言的输入和输出通过标准函数库来实现,通过scanf函数读取键盘输入,键盘输入又成为标准输入,当scanf读取标准输入时,如果还没有输入任何内容,那么scanf 会卡主(专业用语为阻塞)
缓冲区原理:
缓冲区存在于内核区域,进程启动就自动打开标准输入缓冲区,正是因为自动打开才可以读取标准输入。
缓冲区是一段我们不需要申请管理的内存,进程结束操作系统就回收掉这段内存。我们输的每一个数据都在内存里,并且对于驱动,输入的都是字符,键盘是字符设备。
缓冲区其实就是一段内存空间,分为读缓冲,写缓冲,接下来我们来看下c的缓冲三种特性:
1)全缓冲:在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。全缓冲的典型代表是对磁盘文件的读写。(打开一个文件的时候是全缓冲,文件的缓冲区为4096个字节,简称4K,写满后才会往磁盘上写)
2)行缓冲:在这种情况下,当在输入和输出中遇到换行符时,执行真正的I/O操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的l/O操作。典型代表是标准输入(stdin)和标准输出(stdout)。(scanf和printf都是行缓冲,加了\n才会刷新缓冲区/匹配)
3)不带缓冲:也就是不进行缓冲,标准出错情况stderr是典型代表,这使得出错信息可以直接尽快地显示出来。
ANSI C( C89 )要求缓存具有下列特征:
1)当且仅当标准输入和标准输出并不涉及交互设备时,它们才是全缓存的。
2)标准出错决不会是全缓存的。
#ifndef _STDSTREAM_DEFINED#define stdin (&__iob_func()[0])#define stdout (&__iob_func()[1])#define stderr (&__iob_func()[2])#define _STDSTREAM_DEFINED#endif//三个缓冲区:stdin标准输入 stdout标准输出 stderr标准错误输出
下图的例子我们往标准输入缓冲中放入的字符为20\n,当我们输入了\n(回车)后,scanf才开始匹配,scanf 的%d匹配整型数20,然后放入变量i中,我们进行打印输出,这时候\n,仍然在标准输入缓冲区(stdin)内,如果第二个scanf 为scanf(“%d”,&i),那么依然会发生阻塞,因为scanf 在读取整型数,浮点数,字符串时,会忽略\n(回车)、空格等字符(所谓的忽略就是scanf 执行时会首先删除这些字符然后再阻塞)。
缓冲区为空时才会阻塞.
scanf匹配一个字符,就会在缓冲区删除对应字符。因为scanf(“%c”,&c)时,不会忽略任何字符,所以scanf(“%c”,&c)读取了还在缓冲区中残留的\n。

(对应ASCII码10 \n)
scanf中的%d会忽略并且清除\n和空格
2.9.2 scanf循环读取
如果我们想输入多个整数(每次输入都回车),让 scanf读取后,并打印输出,那么我们需要一个while循环,并加入fflush(stdin)(vs2013以上版本需要使用rewind(stdin))
fflush具有刷新(清空)标准输入缓冲区的作用。
如果我们输错了,输入的为字符,scanf无法匹配成功,scanf没有匹配成功其返回值为0,也就是ret的值为0,但是并不等于EOF,因为EOF的值为-1,仍然会进入循环,就会造成不断的打印。
#include<stdio.h>#include<stdlib.h>//vs2013及以上版本要按多次ctrl+zint main(){int i,ret;while(fflush(stdin),(ret=scanf("%d",&i))!=EOF)//返回值为读取成功返回值的个数,读取错误返回0,读取ctrl+z返回-1{printf("i=%d\n",i);}system("pause");}
当读取字符串并打印对应字符串的大写字母时,原让scanf每次读取一个字符,并打印,由于我们一次性输入一个字符串,然后回车,scanf 是循环匹配,所以不能加fflush (stdin),加了以后就会导致第一个字符匹配以后,后面的字符被清空。
#include<stdio.h>#include<stdlib.h>int main(){char c;while(scanf("%c",&c)!=EOF){if(c!='\n'){c = c-32;printf("%c",c);}else{printf("%c",c);}}system("pause");}
2.9.3多种数据类型混合输入
当我们让 scanf一次读取多种类型数据时,对于字符型要格外小心,因为当一行数据中存在字符型数据读取时,读取字符并不会忽略空格、\n(回车)
编写代码时,我们一般需要将%d,与%c之间加入一个空格。
scanf 匹配成功了4个成员,所以返回值为4,我们可以通过返回值,判断scanf匹配成功了几个,中间任何有一个成员匹配出错,那么后面的成员都会匹配出错。
#include<stdio.h>#include<stdlib.h>int main(){int i,ret;char c;float f;ret=scanf("%d %c%g",&i,&c,&f);printf("%d %c %5.2f\n",i,c,f);//5.2代表总结占用5个空格的位置,2代表小数点后显示两位system("pause");}
但scanf按%f匹配数据,会以4个字节读取数据,但double类型占8个字节,需要用%lf匹配数据
2.9.4getchar介绍
通过getchar可以一次从标准输入读取一个字符,等价于
char c,
scanf(%c”,&c)
语法:
| #include |
|---|
#include<stdio.h>#include<stdlib.h>int main(){char c;c=getchar();putchar(c);c=getchar();//不会阻塞putchar(c);system("pause");}
2.9.5putchar介绍
输出字符数据使用putchar函数,作用是向显示设备输出一个字符
语法:
| #include |
|---|
参数ch为要输出的字符,可以字符变量,可以是整型变量,也可以是常量。
2.9.6printf介绍
printf 函数可以输出各种类型数据,整型,浮点型,字符型,字符串等,实际原理是printf将这些类型的数据格式化为字符串后,放入标准输出缓冲区,然后通过\n,刷新标准输出,推到屏幕上。
语法:
| #include |
|---|
| Code | 格式 |
|---|---|
| %c | 字符 |
| %d | 带符号整数 |
| %i | 带符号整数 |
| %e | 科学计数法, 使用小写”e” |
| %E | 科学计数法, 使用大写”E” |
| %f | 浮点数 |
| %g | 使用%e或%f中较短的一个 |
| %G | 使用%E或%f中较短的一个 |
| %o | 八进制 |
| %s | 一串字符 |
| %u | 无符号整数 |
| %x | 无符号十六进制数, 用小写字母 |
| %X | 无符号十六进制数, 用大写字母 |
| %p | 一个指针 |
| %n | 参数应该是一个指向一个整数的指针 |
指向的是字符数放置的位置
| | %% | 一个’%’符号 |
一个位于一个%和格式化命令间的整数担当着一个最小字段宽度说明符,并且加上足够多的空格或0使输出足够长. 如果你想填充0,在最小字段宽度说明符前放置0. 你可以使用一个精度修饰符,它可以根据使用的格式代码而有不同的含义.
用%e, %E和 %f,精度修饰符让你指定想要的小数位数. 例如,
%12.6f
将会至少显示12位数字,并带有6位小数的浮点数.
- 用%g和 %G, 精度修饰符决定显示的有效数的位数最大值.
- 用%s,精度修饰符简单的表示一个最大的最大长度, 以补充句点前的最小字段长度.
所有的printf()的输出都是右对齐的,除非你在%符号后放置了负号. 例如,
%-12.4f
将会显示12位字符,4位小数位的浮点数并且左对齐. 你可以修改带字母l和h%d, %i, %o, %u和 %x 等类型说明符指定长型和短型数据类型 (例如 %hd 表示一个短整数). %e, %f和 %g 类型说明符,可以在它们前面放置l指出跟随的是一个double. %g, %f和 %e 类型说明符可以置于字符’#’前保证出现小数点, 即使没有小数位. 带%x类型说明符的’#’字符的使用, 表示显示十六进制数时应该带’0x’前缀. 带%o类型说明符的’#’字符的使用, 表示显示八进制数时应该带一个’0’前缀.
2.10运算符与表达式
2.10.1运算符分类
C语言提供了13种类型的运算符,如下:
⑴算术运算符(+-*/%)
⑵关系运算符(><==>=<=! =)⑶逻辑运算符(! &&l)
(4)位运算符(<< >>~|入&)
(5赋值运算符(=及其扩展赋值运算符)
(6)条件运算符(? : )
(7)逗号运算符(,)
(8)指针运算符(和&)
(9)求字节数运算符(sizeof)
*(10)强制类型转换运算符((类型))//在混合运算已经讲解
(11)分量运算符(.->)
(12)下标运算符([])
(13)其他(如函数调用运算符() )
2.10.2算术运算符及算术表达式
算术运算符包含+-/%,当一个表达式同时出现这5种运算符时,先进行乘(),除(/),取余(%),取余也叫取模,后进行加(+),减(-),也就是乘除取余的运算符的优先级高于加减运算符。除了%运算符,其余几个运算符即适用于浮点数类型又适用于整型类型,当操作符的两个操作数都是整型数时,它执行整除运算,在其他情况下执行浮点数除法。%为取模操作符,它接受两个整型操作数,把左操作数除以右操作数,但它的值返回的是余数而不是商。
》输入一个整数,然后逆序将其输出。
#include<stdio.h>#include<stdlib.h>int main(){int i,remainder;scanf("%d",&i);while(i)//i!=0{remainder=i%10;printf("%c",remainder+48);i=i/10;}system("pause");}
思考题1:
有两个整型变量a与b,假如不使用第三个变量,交换变量a和 b的值,如何做?
| a=10,b=5; a=a+b;//a是和,容易造成溢出(后期用异或) b=a-b; a=a-b; |
|---|
思考题2:
因为我们的CPU做浮点运算能力不够,王者荣耀中,不再做小数运算,全部用分数代替,如何比较两个分数的大小(不能用除法,除成小数比较)?
| 12/85和30/107的大小? 12107-8530 <0则 12/85更小 >0则 12/85更大 |
|---|
思考题3:
如果输入一个整数,判断该整数是不是对称数,如何做,比如 12321,是对称数,123321也是对称数,但是456就不是对称数。
| 取位、存取 |
|---|
2.10.3关系运算符与关系表达式
关系运算符( > < == >= <= !=)依次为大于,小于,是否等于,大于等于,小于等于,不等于六个运算符,由关系运算符组成的表达式,我们称为关系表达式,关系表达式的的最终值,只有真和假,对应的值为1和0,因为C语言是没有布尔类型(c++/java有)的,在C语言中,0值代表假,非0即为真。例如3>4,这个关系表达式为假,那么其整体的值为0,而5>2这个关系表达式为真,其整体的值为1。关系运算符的优先级低于算术运算符
在工作中,因为很多程序员容易将两个等号,写成一个等号,所以当我们判断整型变量i是否等于3时,我们要这样写3==i,把常量写在前面,这是为了防止不小心将两个等号写为一个等号时,变量在前面就会编译不通,从而快速发现错误。(这种写法属于华为内的一条编程规范)
同时编写程序时,当我们判断三个数是否相等时,绝对不可以写if(S==5==5),这种无论如何都是为假,为什么,因为首先5==5得到的结果为1,然后1==5得到的结果为0
比较3个变量a,b,c是否相等,不能写a==b==c,而应该写成a==b&&b==c来判断三个数是否相等。
#include<stdio.h>#include<stdlib.h>int main(){int i;while(scanf("%d",&i)!=EOF){if(3<i&&i<10)//if(3<i<10){printf("i is between 3 and 10\n");}else{printf("i is not between 3 and 10\n");}}system("pause");}
2.10.4逻辑运算符与逻辑表达式
逻辑运算符( ! && || )依次为逻辑非,逻辑与,逻辑或,和大家学的数学上的与或非是一致的,逻辑非的优先级高于算术运算符,逻辑与和逻辑或的优先级低于关系运算符。逻辑表达式的的最终值,只有真和假,对应的值为1和0。下为如何计算一年是否为闰年的例子,因为重复测试,所以我们用了一个循环。
#include<stdio.h>#include<stdlib.h>int main(){int year;while(scanf("%d",&year)!=EOF){if(year%4==0&&year%100!=0||year%400==0){printf("%d is leap year\n",year);}else{printf("%d is not leap year\n",year);}}system("pause");}
短路运算
| j=1; j==0&&printf(“system is error\n”);//短路操作/为假时短路 j=1; j==1||printf(“system is ok\n”);//短路操作/为真时短路 j==1||(i=j);//i不会被赋值 |
|---|
j=1时,判断j==0表达式为假,中间为与,无论后面真假,整体都为假,所以后面的printf函数表达式不会再执行,因此看不到打印输出,j=0时,j==0为真,所以后面的printf得不到打印,工作中我们经常用短路运算避免使用if判断,降低代码量。
| j=10;//逻辑非运算符 i=!!j; printf(“i的值=%d\n”,i); |
|---|
代码中的逻辑非,首先给j赋值为10,因为j是非0,所以!j为0,然后逻辑非是单目运算符,从右至左,所以!!j得到的值为1(对0取非,得到的值为1,对非0值取非,得到的值为0)
2.10.5位运算符
位运算符( << >> ~ | ^ &)包含左移,右移,按位取反,按位或,按位异或,按位与。
(计算机单位:1个字节=8位 1byte=bit 1KB=1024个字节 1MB=1024KB 1GB=1024MB)
(硬盘厂商:1G=100010001000个字节)
(光纤100M bit 下载速度12.5M)
左移为高位丢弃,低位补0,相当于乘2
工作中很多时候申请内存会用左移,例如申请1G大小的空间,使用malloc (1<<30) (1<<20 1M 1<<10 1K)
(移位比乘法和除法的效率要高)
右移为低位丢弃,正数,高位补0,负数,高位补1
正数,相当于除2.(无符号数也属于整数,最高位为1但补0)
| 原值 0110 0100 0x64 100 0110 0101 0x65 101 0110 0011 0x63 99 右移 0011 0010 0x32 50 0011 0010 0x32 50 0011 0001 0x31 49 ** |
|---|
负数右移,对于偶数来说是除2,但是对于奇数来说是先减1再除2,例如-8>>1,得到的是-4,但是对于-7>>1并不是-3,而是-4
对于-1来说,无论右移多少位,值永远为-1;
| 原值 1111 … 1111 1001 1100 ff…ff9c -100 1111 … 1111 1001 1101 ff…ff9d -99 右移 1111 … 1111 1100 1110 ff…ffce -50 1111 … 1111 1100 1110 ff…ffce -50 |
|---|
按位取反即对于位是1的变为0,位是0的变为1( 补码为取反加1 取反为负数减一)
按位与和按位或,就是用两个数的每一位进行与和或。
| 原值 0000 0000 0000 0101 (short)5 原值 0000 0000 0000 0101 (short)5 原值 0000 0000 0000 0111 (short)7 原值 0000 0000 0000 0111 (short)7 或 0000 0000 0000 0111 (short)7 与 0000 0000 0000 0101 (short)5 |
|---|
思考题:如何找出最低位为1的位数
| 1.循环:和1按位与 左移循环 2.取反 按位与 i&-i |
|---|
异或 相同的数,异或为0,不同的数异或为1,任何数和0异或得其本身。(满足交换律)
| i=i^j;//i和j交换 但不能自身与自身交换 j=i^j; i=i^j; |
|---|
思考题:101个数,50个数出现2次,1个数出现1次,出现1次的这个数的值
#include<stdio.h>#include<stdlib.h>int main(){int arr[5]={9,11,7,9,11};int i,result=0;for(i=0;i<sizeof(arr)/sizeof(int);i++){result^=arr[i];}printf("find value=%d\n",result);system("pause");}
思考题:
102个数,50个数出现2次,2个数出现1次,出现1次的这个数的值(分堆)
103个数,50个数出现2次,2个数出现1次,出现1次的这个数的值(分堆)
-
2.10.6赋值运算符
左值(L-value)就是那些能够出现在赋值符号左边的东西。右值(R-value)就是那些可以出现在赋值符号右边的东西。这里有个例子:
a= b+25;
a是个左值,因为它标识了一个可以存储结果值的地点, b+25是个右值,因为它指定了一个值。
b+25 = a;(错误表达式)
原先用作左值的a 此时也可以当作右值,因为每个位置都包含一个值。然而,b+25不能作为左值,因为它并未标识一个特定的位置(并不对应特定的内存空间)。因此,这条赋值语句是非法的。
赋值运算符的优先级是非常低的,仅高于逗号运算符。
复合赋值运算符操作是一种缩写形式,使得对变量的赋值操作变的更加简洁。
赋值运算符与复合赋值运算符的区别在于后者:
1、简化了程序,使程序精炼,阅读速度提升。
2、提高了编译效率2.10.7条件运算符与逗号运算符
条件运算符是c语言中唯一的三目运算符,三目运算符代表有三个操作数,双目运算符就是两个操作数,比如我们的逻辑与就是双目运算符,单目运算符就是一个操作数,比如逻辑非就是单目运算符,运算符我们也可以称为操作符。
逗号运算符的优先级最低,我们需要掌握的是逗号表达式整体的值是最后一个表达式的值
通过条件运算符我们可以快速得到3个数中间的最大值,避免了很多if判断 ```cinclude
include
int main() { int a,b,c,max; while(fflush(stdin),scanf(“%d%d%d”,&a,&b,&c)!=EOF) //fflush(stdin)避免错误输入导致无限循环 { max=a>(b>c?b:c)?a:(b>c?b:c); //max=a>b?(a>c?a:c):(b>c?b:c) printf(“max=%d\n”,max); } system(“pause”); }
通过逗号运算符,我们可以先做一些准备操作,而最终 while 循环是否结束,依赖scanf("%d%d%d",&a,&b,&c)!=EOF 这个关系表达式的真假。| a=b,c;//赋值运算符优先级大于逗号运算符 a为b的值<br />a=(b,c);//a为c的值 || --- |<a name="d2CDd"></a>### 2.10.8自增、自减运算符及求字节运算符自增自减运算符和其他运算符有很大的区别,因为其他运算符除了赋值运算符可以改变变量本身的值以外,其他的运算符不会有这种效果,自增自减就是对变量进行加1和减1操作,有加法和减法运算符,为什么还要发明这个呢,其实是因为自增和自减来源于B语言,当时汤姆逊和里奇(C语言的发明者)为了不改变当时程序员的编写习惯,所以在c中保留了下来。因为自增自减会改变变量的值,所以自增和自减是不能应用于常量。- 自增自减注意分清是前还是后- 针对后加加,后减减| j=i-->-2;//等价于j=i>-2;i--; || --- |- sizeof不是函数,是C的关键字,是一个运算符<a name="ceIOP"></a># 第3章选择与循环<a name="m2jJI"></a>## 3.1选择结构程序设计<a name="0Ewfy"></a>### 3.1.1关系表达式与逻辑表达式在讲选择语句之前,我们首先来练习一下关系表达式与逻辑表达式,**算术运算符优先级高于关系运算符,关系运算符优先级高于逻辑与与逻辑或,相同优先级运算符从左至右**进行结合<br />5>3&&8<4-!0 <br />计算如图<br /><a name="sFpfa"></a>### 3.1.2 if语句在计算机中,我们用if判断语句来实现这样的效果,if判断条件(表达式)为真,就执行某个语句,如果为假,就不执行这个语句。当然也可以if判断条件〈表达式)为真,就执行某个语句,如果为假,用else 分支执行另一个语句- if的括号后面不能加分号同时 if 也支持if 与else if 功能,无论有多少个语句,或者else if,程序**只会执行其中一个语句**。同时if也支持多层嵌套,在if语句中又包含一个或多个if语句称为if语句的嵌套。<br /><a name="QLEEk"></a>### 3.1.3 switch 语句当一个变量我们判断其等于几个值,或者几十个值时,使用if和 else if 会导致else if分支非常多,这种情况我们将使用switch 语句<br />_语法:_| switch( expression ) { case A: statement list; break; ... case N: statement list; break; default: statement list; break; } || --- |- **switch中不能写浮点型**<a name="gWJrH"></a>## 3.2循环结构程序设计<a name="Rn3E8"></a>### 3.2.1 goto 语句在学校老师都讲不用掌握goto,这种说法是不对的,goto才是循环的本质,对应于汇编中的jmp跳转,C中的while,do while和 for在程序编译时,都要拆解为汇编的jmp。<br /> goto语句——无条件转向语句,使用方法为goto语句标号;语句标号的命名规则与C语言变量的命名规则一致,例如: goto label_1;//合法<br />goto 123;//不合法<br />goto的使用场景分为两种,一种是**向上跳转**,实现循环,一种是**向下跳转**,实现中间部分代码不执行。<br />如何查看死循环:任务管理器》转到进程》是否占cpu<a name="AADt5"></a>### 3.2.2 while循环while 语句用来实现“当型”循环结构,一般形式:| while (表达式)<br />{<br />语句<br />} || --- |当表达式为非0值时,执行while语句中的内嵌语句。其特点是:先判断表达式,后执行语句。如下图所示,表达式非0,就会执行语句,从而实现语句多次执行的效果,但是程序不能死循环,在语句中需要有让表达式趋近于假的操作。- while循环内没有使条件趋近于假的语句- 不能在while循环后加分号<a name="ZEQb9"></a>### 3.2.3 do while循环do-while 语句的特点:先执行循环体,然后判断循环条件是否成立。<br />一般形式:| do<br />{<br /> 循环体语句<br />}<br />while (表达式)**;** || --- |执行过程:先执行一次指定的循环体语句,然后判别表达式,当表达式的值为非零(“真”)时,返回重新执行循环体语句,如此反复,直到表达式的值等于0为止,此时循环结束。<a name="trLoU"></a>### 3.2.4 for循环c语言中的for语句使用最为灵活,不仅可以用于循环次数已经确定的情况,而且可以用于循环次数不确定而只给出循环结束条件的情况,它完全可以代替while语句。<br />一般形式:<br />for(表达式1;表达式2;表达式3)<br />语句for语句的执行过程:<br />**(1)先求解表达式1。**<br />**(2)求解表达式2,若其值为真(值为非0),则执行for语句中指定的内嵌语句,然后执行下面第(3)步。若为假(值为0),则结束循环,转到第(5)步。**<br />**(3)求解表达式3。**<br />**(4)转回上面第(2)步骤继续执行。**<br />**(5)循环结束,执行for语句下面的语句**```c#include<stdio.h>#include<stdlib.h>//for括号后面加分号,会造成计算错误//break结束循环//continue本次循环, continue后面的语句不再执行//for内部的循环尽量不要省略int main(){int i,sum;for(i=1,sum=0;i<=100;i++);{if(i%2==0){sum+=i;}}printf("sum=%d\n",sum);system("pause");}
3.2.5 continue语句
作用为结束本次循环,即跳过循环体中下面尚未执行的语句,接着进行下一次是否执行循环的判定
当continue使用于while和do while循环时,注意不要跳过让循环趋近于假的语句。
3.2.6 break语句
与continue相比,break语句则是结束整个循环过程,不再判断执行循环的条件是否成立。
第4章数组
4.1一维数组
4.1.1数组定义
一个班学生的学习成绩,一行文字,一个矩阵这些数据的特点是:
1.具有相同的数据类型
2.使用过程中需要保留原始数据
C语言为这些数据,提供了一种构造数据类型:数组。所谓数组就是一组具有相同数据类型的数据的有序集合。
一维数组的定义格式为:
类型说明符 数组名[常量表达式];
例如:int a[10];
它表示定义了一个整形数组,数组名为a,此数组有10个元素。
说明:
1.数组名定名规则和变量名相同,遵循标识符定名规则。
2.在定义数组时,需要指定数组中元素的个数,方括弧中的常量表达式用来表示元素的个数,即数组长度。
3.常量表达式中可以包括常量和符号常量,但不能包含变量。也就是说,c语言不允许对数组的大小作动态定义,即数组的大小不依赖于程序运行过程中变量的值。
以下是错误示例:
int n;
scanf(“%d”,&n);
int a[n];/在程序中临时输入数组的大小/
数组声明中其他常见的错误:
①float a[0];/数组大小为0没有意义/
②int b(2)(3);/不能使用圆括号/
③int k=3, a[k];/不能用变量说明数组大小/
4.1.2一维数组在内存中的存储
数组元素的引用方式是数组名 [下标]
一维数组int mark[100];每个元素都是整型元素,占用4个字节。所以我们访问数组 mark 的 100元素的方式是mark[0]一直到mark[99],注意没有mark[100]元素,数组元素从0编号开始的。
下面来看一下一维数组的初始化方法
1.在定义数组时对数组元素赋以初值。
例如:int a [10] ={0,1,2,3,4,5,6,7,8,9};不能写成int a[10];a[10]={0,1,2,3,4,5,6,7,8,9}
2.可以只给一部分元素赋值。
例如: int a [10] ={0,1,2,3,4};
定义a数组有10个元素,但花括弧内只提供5个初值,这表示只给前面5个元素赋初值,后5个元素值为0
3.如果想使一个数组中全部元素值为0,可以写成:
inta [10] ={0,0,0,0,0,0,0,0,0,0};或int a [10] ={0};
4.在对全部数组元素赋初值时,由于数据的个数已经确定,因此可以不指定数组长度。
例如int a [] ={1,2,3,4,5};
4.1.3 栈空间和数组
调试子函数:F11
主函数中int类型数组长度计算:sizeof(数组名)/sizeof(int)
子函数中数组长度需要单独传递。一维数组在传递的时候,它的长度是传递不过去的。
原因:数组名存储数组的起始地址,传入到子函数中只是传入了起始地址,相当于一个指针。在子函数数组的方括号[]中填写任何数字都是没有意义的。子函数数组的起始地址和 main函数的数组是同一个,在内存中就是同一个位置。
访问越界
函数开始执行时,系统会为其分配对应的函数栈空间。先定义的变量在高地址,后定义的变量在低地址,数组地址起始地址在低地址,但会向高地址增长。函数的栈空间中的,由于后定义的变量在上面,我们把这种效果成为栈先上增长。
stack aroud …栈使用异常/访问越界
stackoverflow 栈溢出
#include<stdio.h>#include<stdlib.h>//print地址空间相对main函数,是低越界void print(int a[],int arrLen){int i;for(i=0;i<arrLen;i++){printf("%3d",a[i]);}}int main(){int arr[5]={1,2,3,4,5};print(arr,5);system("pause");}
4.2二维数组
4.2.1 二维数组的定义与引用
二维数组定义的一般形式为
类型说明符 数组名[常量表达式][常量表达式];
例如:定义a为3×4(3行4列)的数组,b为5×10(5行10列)的数组。如下:float a [3] [4] ,b [5] [10];
注意:我们可以把二维数组看作是一种特殊的一维数组:它的元素又是一个一维数组。例如:可以把二维数组 a[3][4]看作是一个一维数组,它有3个元素: a[0]、a[1]、a[2],每个元素又是一个包含4个元素的一维数组。
二维数组中的元素在内存中的排列顺序是:按行存放,即先顺序存放第一行的元素,再存放第二行的元素,数组元素获取依次是从a[0][0],a[0][1]到最后一个元素a[2][3]。
4.2.2二维数组初始化及传递
1.分行给二维数组赋初值。
例如:int a [3] [4] ={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
2.可以将所有数据写在一个花括号内,按数组排列的顺序对各元素赋初值。
例如: int a [3] [4]={1,2,3,4,5,6,7,8,9,10,11,12};
3.可以对部分元素赋初值。
例如:int a [3] [4]={{1},{5},{9}};效果如下
| 1 0 0 0 5 0 0 0 9 0 0 0 |
|---|
4.如果对全部元素都赋初值,则定义数组时对第一维的长度可以不指定,但第二维的长度不能省。
例如: int a [3] [4]={1,2,3,4,5,6,7,8,9,10,11,12};
它等价于: int a [][4]={1,2,3,4,5,6,7,8,9,10,11,12};
二维数组中每一个元素按顺序存储,从低地址到高地址,大小为sizeof(int)*元素个数,元素个数为行乘以列,总计12个元素,所以大小为48个字节。当执行到print函数时,按F11即可进入print 函数,二维数组在传递时,列数一定要写,因为二维数组传递时也是以指针变量(数组指针)形式传递的,当然列数要与主函数中的二维数组a的列数相同,我们在图4.2.2-3中可以看到arr的地址值与a相等,同时sizeof(arr[0])为16,其实arr[0]代表第一行,是一个一维数组,除以sizeof(int)就可以得到一行有几个元素,二维数组在打印时,外层为行,内层为列,这样可以以矩阵效果打印二维数组每一个元素。
#include<stdio.h>#include<stdlib.h>void print(int arr[][4],int row){int i,j;for(i=0;i<row;i++){for(j=0;j<sizeof(arr[0])/sizeof(int);j++){printf("%3d",arr[i][j]);}printf("\n");}}int main(){int a[3][4]={1,3,5,7,2,4,6,8,9,11,13,15};//访问范围a[0][0] a[2][3]int b[3][4]={{1},{5},{6,9}};print(a,3);system("pause");}
4.3 字符数组
4.3.1字符数组的定义及初始化
定义方法与前面介绍的类似。例如:char c[10];
1.可以对每个字符单独赋值进行初始化
c[0]=’I’ ;c[1]=’ ‘ ;c[2]=’a’ ;c[3]=’m’ ;c[4]=’ ’ ;c[5]=’h’;c[6]=’a’ ;c [7] =’p’ ;c [8] =’p’ ;c [9] =’ v’ ;
2.也可以char c[10]={‘I’,’a’,’ m’,’h’,’a’,’p’,’p’,’ y’}
3.char c[10]=”hello”通常所采用的初始化方式.工作中我们不用以上两种初始化方式,原因是字符数组我们用来存取字符串.因为C语言规定字符串结束标志为’\0’,而系统对字符串常量自动加一个’\0’,为了保证处理方法一致,我们会人为的在字符数组添加’\0’,所以字符数组存储的字符串长度必须必字符数组小一个字节,比如,例如char c[10]最长存储9个字符,剩余1个字符用来存储’\0’
#include<stdio.h>#include<stdlib.h>void print(char c[]){int i=0;while(c[i]){putchar(c[i]);i++;}putchar('\n');}int main(){char c[7]="ABCDEF";char d[10];int i;//printf("%s\n",c);print(c);scanf("%s%s",c,d);//不用取地址,数组名存储的是数组起始地址//scanf读到空格不匹配printf("%s--%s\n",c,d);system("pause");}
当我们赋值的字符串长度和数组长度相等,会打印出现很多烫(当我们未赋值’\0’时微软编译器栈空间的字节大多是cc(0xcc对应字符串是烫)),因为 printf通过%s打印字符串时,原理是依次输出每一个字符,当读到结束符’\0’时,结束打印。
scanf 通过%s读取字符串,我们对c和d分别输入 are you,中间加一个空格, scanf使用%s读取字符串时,会忽略空格和回车,我们通过 print函数模拟实现 printf 的%s 效果,当c[i]为’\0’时,其值也是0,循环结束,当然也可以写为c[i]!=’\0’。
4.3.2gets函数与puts函数
gets 函数类似scanf 函数,用于读取标准输入,前面我们已经掌握 scanf 函数在读取字符串时遇到空格,就认为读取结束,所以当输入的字符串存在空格时,我们使用gets 函数进行读取
char gets( char str );
gets()函数从STDIN(标准输入)读取字符并把它们加载到str(字符串)里,直到遇到新行(\n)或到达EOF,新行字符翻译为一个null中断符。我们输入how are you,总计11个字符,可以看到gets 会读取空格,同时可以看到并未给数组进行初始化赋值,但是最后有’\0’,因为 gets遇到\n后,不会存储\n,而是翻译为’\0’空字符。
int puts( char*str );
函数puts()把 str(字符串)写到STDOUT(标准输出)上.puts()成功时返回非负值,失败时返回EOF.puts会将数组c中存储的 hao are you字符串打印到屏幕上,同时会打印换行,相对于printf 函数,puts 只能用于输出字符串,同时会多打一个换行符。
fgets (gets容易访问越界,fgets接口更安全)
语法:
| #include //fgets(c,sizeof(c),stdin); |
|---|
4.3.3str系列字符串操作函数
str系列字符串操作函数主要包括strlen,strcpy,strcmp,strcat 等函数(所有函数可以加入前言的QQ群,群内有C/C++函数大全),strlen用于统计字符串长度,strcpy用于将某个字符串复制到字符数组,strcmp用于比较两个字符串大小,strcat用于将两个字符串连接到一起。
#include
size_t strlen( char str );
char strcpy( char to,const char from );
int strcmp( const char str1,const char str2 );
char strcat( char str1,const char str2 );针对传参类型为char,直接放入字符数组的数组名即可。
接下来我们通过一个实例来学习str系列函数,掌握每一个函数的内部实现。
#include<stdio.h>#include<stdlib.h>#include<string.h>int main(){char c[10];size_t len;while(fgets(c,sizeof(c),stdin)!=NULL){c[strlen(c)-1]=0;puts(c);len=strlen(c);printf("%d",len);}system("pause");}
strcpy是将字符串中的字符,一个字符一个字符的赋值给目标字符数组,例子中我们将c复制给d,就是将c中的每一个字符依次赋值给d,也会将结束符赋值给d。注意目标数组一定要大于字符串大小,也就是sizeof(d)>strlen(c),否则会造成访问越界。
- strcpy不能用于整型数组
strcpy遇到00则结束
strcmp比较两个字符串的大小,由于字符数组c中的字符串与d相等,所以返回0,如果c 中的字符串大于d,那么返回值为1,如果c中的字符串小于d,那么返回值为-1。从头开始比较,比较相同位置字符的ASClIl码值,如果发现不相等就会直接返回,否则会接着往后比较,例如 strcmp(“hello””how”),返回值就是-1,也就是hello小于how,因为第一个字符h相等,就接着往后比,e的ASCIl码小于o,然后就返回-1。
strcat是将一个字符串接到另外一个字符串的末尾,例子中字符数组c中为hello,我们将d中的 world拼按,最终结果为helloworld,注意目标数组必须大于拼接后的字符串大小,也就是sizeof(c)>strlen(“helloworld””)。
4.3.4stm系列字符串操作函数
strn系列函数包括strncpy,strncmp,strncat,当我们只想复制一部分源字符串的字符时,我们需要用strncpy,当只需要比较两个字符串的一部分是否相等时,我们使用strncmp,当需要将一个字符串的部分字符拼接到另一个字符串时,我们需要使用strncat,下面是这三个函数的用法:
char strncpy( char to, const char*from, size_t count );
功能:将字符串from中至多count个字符复制到字符串to中。如果字符串 from 的长度小于count,其余部分用‘\0’填补。返回处理完成的字符串。
int strncmp( const char str1, const char str2, size_t count );
功能:比较字符串str1和str2中至多count个字符。
如果参数中任一字符串长度小于count,那么当比较到第一个空值结束符时,就结束处理。
char strncat( char str1, const char *str2, size_t count );
功能:将字符串from中至多count个字符连接到字符串to中,追加空值结束符。返回处理完成的字符串。
(一个汉字两个字节)
4.3.5mem系列操作函数
虽然放到字符数组这一节,但是我们的mem系列函数是任何数组都可以进行操作的,无论是字符数组,还是整型数组,浮点型数组,或者后面我们讲的结构体数组。
当需要对一个数组进行全部置位零时,我们需要使用到memset,接口如下:
void memset( void buffer, int ch, size_t count );
功能:函数拷贝ch 到 buffer从头开始的count个字符里,并返回 buffer 指针memset()可以应用在将一段内存初始化为某个值。例如:
memset( the_array, ‘\0’ , sizeof(the_array) );
这是将一个数组的所有元素设置成零的很便捷的方法。
当我们需要把一个整型数组,或者浮点型数组的数据,或者某一部分元素的数据搬到另外一个数组时,这时我们不能使用strcpy,需要使用memcpy,接口如下:
void memcpy( voidto, const void *from, size_t count );
功能:函数从from中复制count个字符到to中,并返回to指针。如果 to和from重叠,则函数行为不确定。
当我们复制的内容发生重叠时,我们需要使用memmove,不能使用memcpy,接口如下:
void memmove( void to, const void *from, size_t count );
功能:与mencpy相同,不同的是当to和from重叠,函数正常仍能工作。
int memcmp( const void buffer1, const void buffer2, size_t count );
功能:函数比较buffer1和 buffer2的前count个字符。
memcmp可以比较任何类型数组,当然主要是比较两个数组是否相同,其内部原理实际是一个字节一个字节比较大小的,因此一般用于比较相等。count 是用来控制比较的字节数目。
第5章指针
5.1指针的本质
5.1.1指针的定义
内存区的每一个字节有一个编号,这就是“地址”。如果在程序中定义了一个变量,在对程序进行编译时,系统就会给这个变量分配内存单元。按变量地址存取变量值的方式称为“直接访问”方式,例如 printf(%d”,i);scanf(“%d”,&i);另一种存取变量值的方式称为“间接访问”的方式。即,将变量i的地址存放在另一个变量中。在C语言中,指针变量是一种特殊的变量,它是存放地址的。
定义一个指针变量 基类型指针变量名;例如int i_pointer;
指针与指针变量是两个概念,一个变量的地址称为该变量的“指针”。例如,地址2000是变量i的指针。如果有一个变量专门用来存放另一变量的地址(即指针),则它称为“指针变量”。上述的i_pointer就是一个指针变量。
i_pointer本身占据的内存空间因为我们后面写的都是win32控制台应用程序,寻址范围为32位,即4个字节,如果编写的程序是64位,那么就是8个字节。所以对于我们来说sizeof(i_pointer)等于4。
编址
4个字节 32位
8个字节 64位
5.1.2取地址操作符与取值操作符
取地址操作符是&,也叫引用,通过该操作符我们可以获取一个变量的地址值;取值操作符为,也叫解引用,通过该操作符我们可以拿到一个地址对应位置的数据。
如图所示,我们通过&i获取整型变量i的地址值,然后对整型指针变量p进行初始化,p中存储着整型变量i的地址值,所以通过第12行的p,我们就可以拿到整型变量i的值,p里边存储的是一个绝对地址值,为什么取值时,其会获取四个字节大小的空间呢,是因为p为整型变量指针,int占用4个字节大小的空间,所以p在解引用时会访问四个字节大小的空间,同时以整型值对内存进行解析。
注意以下两点
1、指针变量前面的“*”,表示该变量的类型为指针型变量。
例: float pointer_1;
指针变量名是pointer_1 ,而不是 pointer1 。
2、在定义指针变量时必须指定基类型。
需要特别注意的是,只有整型变量的地址才能放到指向整型变量的指针变量中。下面的赋值是错误的︰
floata;
int * pointer_1;
pointer_1=&a;1/毫无意义,而且会造成出错,有兴趣的小伙伴可以自行尝试3、如果已执行了语句pointer 1 =&a ;
&pointer_1的含义是什么?
“&”和“”两个运算符的优先级别相同,但按自右而左方向结合。因此,&pointer_ 1 与&a相同,即变量a的地址,也就是pointer_1。
&a 的含义是什么?
先进行& a运算,得a的地址,再进行运算。& a和pointer_1的作用是一样的,它们都等价于变量a。即& a 与a等价。
5.2指针的使用场景
很多同学很害怕指针,觉得很容易用错,其实是因为没有掌握指针的使用场景,指针的使用场景通过总结,只有两个,传递与偏移,在这两个场景下采用指针,这样就可以准确使用指针,你就会发觉指针其实很简单。
5.2.1指针的传递
首先我们来看下面一个例子,主函数中我们定义了整型变量i,初始化值为10,我们想通过子函数修改整型变量i的值,但是你会发现 after change后,打印的值仍为10,子函数change并没有改变变量i的值,为什么呢,我们通过执行来查看。
#include<stdio.h>#include<stdlib.h>void change(int j){j=j/2;}int main(){int i=10;printf("before chande i=%d\n",i);change(i);printf("after change i=%d\n",i);system("pause");}
5.2.2指针的偏移
前面有指针的传递,指针即地址,就像你找到了一栋楼,该栋楼叫B栋,那么往前就是A栋,往后就是c栋,所以指针的另一个场景就是对其进行加和减,地址进行乘除是没有意义的,就像你家的地址乘5是代表什么,没有意义。工作中,我们把对指针的加减,称之为指针的偏移,加就是向后偏移,减就是向前偏移。下面我们来看一个实例
#include<stdio.h>#include<stdlib.h>#define N 5int main(){int a[N]={1,2,3,4,5};//数组名里存储的是数组的起始地址int *p;int i;p=a;for(i=0;i<N;i++){printf("%3d",*(p+i));}printf("\n");p=&a[4];for(i=0;i<N;i++){printf("%3d",*(p-i));}printf("\n");system("pause");}
5.2.3指针与自增自减运算符

#include<stdio.h>#include<stdlib.h>int main(){int a[3]={2,7,8};int *p;int j;p=a;j=*p++;//j=*p;p++;后++第一步去掉++;printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p);//2,2,7j=(*p)++;//j=(*p),(*p)++;//等价于j=p[0]++printf("a[0]=%d,j=%d,*p=%d,a[1]=%d\n",a[0],j,*p,a[1]);//2,7,8system("pause");}
5.2.4指针与一维数组
一维数组在函数调用进行传递时,它的长度子函数无法知道呢。其实一维数组名中存储的是数组的首地址,如下图,数组名c中存的地址为0x0015faf4,所以在子函数change中我们接受一个地址,可以定义一个指针变量,指针变量的基类型要和数组的数据类型保持一致,通过取值操作,就可以将h 改为H,称为指针法,获取数组元素,也可以用取下标获取数组元素进行修改,我们称之为下标法。
#include<stdio.h>#include<stdlib.h>void change(char *p){*p='H';p[1]='E';*(p+2)='L';}int main(){char c[10]="hello";change(c);puts(c);system("pause");}
5.2.5指针与动态内存申请
我们定义的整型,浮点型,字符型变量,数组变量,都在栈空间,栈空间的使用是在编译时确定大小,如果使用的空间大小不确定,那么我们就需要使用堆空间。下面我们来看一个实例
首先我们来看下malloc函数,#include
如下所示,我们定义的整型变量i,指针变量p均在main 函数的栈空间,通过malloc申请的空间会返回一个堆空间的首地址,我们把首地址存入变量p中,知道了首地址,我们就可以通过strcpy往对应的空间里放字符数据。
既然都是内存空间,为什么还要分栈和堆呢?栈(先进后出)是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
(执行程序的时候,变量特别多。单核cpu寄存器硬件的数目有限,访问每一个变量的原理是栈零指针的偏移。)
#include<stdio.h>#include<stdlib.h>#include<string.h>int main(){int needSize;char *pStart;scanf("%d",&needSize);pStart=(char*)malloc(needSize);strcpy(pStart,"hello");puts(pStart);free(pStart);pStart=NULL;//要把free后的指针置为NULLsystem("pause");}
栈空间由系统自动管理,而堆空间申请和释放需要我们自行管理,所以在例子中我们需要通过free进行堆空间的释放, free函数头文件及格式为include
#include<stdio.h>#include<stdlib.h>#include<string.h>int main(){int *p1,*p2,*p3;p1=(int*)malloc(4);*p1=1;printf("*p1=%d\n",*p1);p2=(int*)malloc(4);*p2=2;printf("*p2=%d\n",*p2);free(p2);p3=(int*)malloc(4);*p3=3;printf("*p3=%d\n",*p3);//3*p2=100;printf("*p3=%d\n",*p3);//100system("pause");}
free以后没有设置为NULL的指针称为野指针
接下来我们看一个栈空间和堆空间在时间的有效性的差异,下面的例子,第二次打印会有异常,而且每次执行打印效果都不一样,原因是print_stack 函数中的字符串存放在栈室间,当函数执行结束后,栈空间会被释放,字符数组c原有空间已经被分配至其他函数使用。而print_malloc函数中字符串存放在堆空间,堆只有我们free才会释放,否则在进程执行过程中一直有效。
#include<stdio.h>#include<stdlib.h>#include<string.h>//函数执行完毕,栈空间释放char *printStack(){char c[]="I am printStack";puts(c);return c;}char *printMalloc(){char *p=(char*)malloc(20);strcpy(p,"I am printMalloc");puts(p);return p;}int main(){char *p;p=printStack();//打印会乱码puts(p);p=printMalloc();puts(p);system("pause");}
realloc
语法:
| #include void realloc( void ptr, size_t size ); |
|---|
功能: 函数将ptr 对象的储存空间改变为给定的大小size。 参数size可以是任意大小,大于或小于原尺寸都可以。 返回值是指向新空间的指针,如果错误发生返回NULL。
#include<stdio.h>#include<stdlib.h>#include<string.h>#define CAPACITY 20int main(){char c;int i=0,cap=CAPACITY;char *p=(char*)malloc(CAPACITY);while(scanf("%c",&c)!=EOF){if(i==cap-1){cap=2*cap;p=(char*)realloc(p,cap);}p[i]=c;i++;}p[i]=0;puts(p);system("pause");}
5.2.6字符指针与字符数组的初始化
字符指针可以初始化赋值一个字符串,字符数组初始化也可以赋值一个字符串,如下面例子所示,char p=”hello”和 char c[10]=”hello”有什么区别呢,如图5.2.6-1所示,编译器在时,对于字符串常量是存储在数据区中的常量区的,好处是相同的字符串,比如 hello只会存储一遍,常量区的含义就是字符串本身是不可修改的,所以我们称之为字符串常量,hello存在字符串常量区,占用6个字节,有自己的首地址,char p=”hello”是将字符串常量”hello”的首地址赋值给p,对于 char c[10]=”hello”,字符数组c在栈空间有10个字节大小的空间,这个初始化是将字符串 hello通过strcpy给字符数组c,因此我们可以将c[0]修改为H,而p[0]拿到是常量区的空间,所以不可以修改。
p是一个指针变量,因此我们可以将字符串 world的首地址重新赋值给p,而数组名c本身存储的就是数组的首地址,是确定的,不可修改的,c 等价于符号常量,因此如果c=”world”打开就会造成编泽不通。
5.2.7深入理解const
5.2.8memcpy 与 memmove的差异
5.3数组指针与二维数组
5.3.1数组指针应用
很多同学在学习C语言时,都认为二维数组和二级指针是一回事,二维数组的实现是用二级指针偏移实现的,这是错误的!二维数组通过两次偏移获取到数组中的某一个元素,所使用的指针是数组指针,数组指针是一级指针。
#include<stdio.h>#include<stdlib.h>#include<string.h>void print(int (*p)[4],int row){int i,j;printf("sizeof(p)=%d\n",sizeof(p));//只要是指针在32位就是4个字节printf("sizeof(*p)=%d\n",sizeof(*p));//*p表示一个一维数组for(i=0;i<row;i++){for(j=0;j<sizeof(*p)/sizeof(int);j++){printf("%3d",*(*(p+i)+j));//printf("%3d",*(p[i]+j));//printf("%3d",p[i][j]);}printf("\n");}}//数组指针 传递二维数组int main(){int a[3][4]={1,3,5,7,2,4,6,8,9,11,13,15};int (*p)[4];//指向长度为4的一维数组int *p1;int b[4]={1,2,3,4};p=a;print(p,3);system("pause");}
p是一个数组指针,其指向一个大小为4个整型元素的数组,所以p代表一个长度为4的整型数组,通过sizeof(p)可以看到其大小为16个字节,前面我们讲过指针的偏移,p+1偏移的长度为其基类型的大小,因为p指向一个大小为4个整型元素的数组,所以p+1偏移16个字节,因为二维数组名a中存储的地址类型为数组指针,所以我们将a赋值给p不会有编译警告。
通过print函数将二维数组打印成矩阵形式,当然我们可以把形参中的int p[][4]改为int ()[4],是等价的,二维数组的行数依然无法传递过去的,所以我们通过整型变量row传递行数,对于打印位置的 pli][j],我们可以写成((p+i)+j),当然对于使用二维数组,我们使用p[i][j]较多,首先p+i偏移到对应的行,然后(pti)就是拿到对应行,等价于一维数组,而一维数组的数组名存储的就是一级指针,所以*(p+i)+j就偏移到对应的元素,然后再解引用就拿到对应的元素值。
5.3.2二维数组的偏移计算
定义int a[3][4]=(1,3,5,7,9,11,13,15,17,19,21,23};假设a 的地址, 0x2000,那么下表展示了各种写法偏移后对应的地!值,如果定义数组指针变量 int (和p)[4],将 p=a,那么下面第一列表达形式中,把a换出p,一切成立。
5.4二级指针
一级指针的使用场景是传递与偏移,服务的对象是整型变量,浮点型变量,字符型变量等,那么二级指针既然是指针,其作用也是传递与偏移,服务对象更加简单,只服务于一级指针的传递与偏移!
5.4.1二级指针的传递
下面有一个实例,整型指针 pi,指向整型变量i,整型指针pj指向整型变量j,通过子函数change,我们想改变指针变量pi的值,让其指向j,我们知道c语言的函数调用是值传递,因此要想在change 中改变变量pi的值,那么必须把pi的地址传递给change,如图5.4.1-1所示,pi是一级指针,&pi的类型即为2级指针,左键拖至内存区域可以看到,Ox0031F800就是指针变量pi本身的地址,对应存储的地址是红色标注位置1整型变量i的地址值(因为小端所以低位在前),将其传入函数 change,change函数形参p必须定义为二级指针,然后在 change 函数内,对p进行解引用,就可以拿到pi,进而对其存储的地址值进行改变。
对于二级指针的传递使用场景,把握两点,第一:二级指针变量定义是在形参,第二:在调用函数中往往不定义二级指针,如果定义,初始化注意是一级指针的取地址。
#include <stdio.h>#include <stdlib.h>void change(int **p,int* pj){int i=5;*p=pj;}//要想在子函数改变一个变量的值,必须把该变量的地址传进去//要想在子函数改变一个指针变量的值,必须把该指针变量的地址传进去int main(){int i=10;int j=5;int *pi;int *pj;pi=&i;pj=&j;printf(" i=%d,*pi=%d,*pj=%d\n",i,*pi,*pj);//等于10change(&pi, pj);printf("after change i=%d,*pi=%d,*pj=%d\n", i,*pi, *pj);
5.4.2二级指针的偏移
一级指针的偏移服务于数组,整型一级指针服务于整型数组,所以二级指针的偏移也服务于数组,服务对象为指针数组,请看下面实例,实际在淘宝购物过程中,大家搜索的商品信息存在内存中,如果以某个查询条件搜索商品,淘宝需要把商品按你的要求进行排序,比如价格从低到高,这时交换内存中商品的信息会极大的降低效率,因为不同用户会有不同的查询需求,每件商品本身的信息存储又较大,假如我们让每个指针指向商品信息,排序比较时,我们比较实际的商品信息,但交换的是指针,这样交换成本将会极大降低,这种思想称为索引式排序,下面的例子把字符串看成商品信息即可,将指针数组p赋值给二级指针p2,目的是为了演示二级指针的偏移,p2+1偏移的就是一个指针变量空间的大小,即sizeof(char*),对于win32控制台应用程序,就是偏移4个字节。
#include<stdio.h>#include<stdlib.h>#include<string.h>#define N 5//排序指针数组void print(char (*p)[10]){int i;for(i=0;i<N;i++){puts(p[i]);}}void printPointArr(char **p){int i;for(i=0;i<N;i++){puts(p[i]);}}int main(){char *pArr[N];char b[N][10]={"pengyu","xieshuang","guo","yu","sibei"};int i,j;char *pTmp;char **p2;for(i=0;i<N;i++){pArr[i]=b[i];}print(b);for(i=N;i>1;i--){for(j=0;j<i-1;j++){if(strcmp(pArr[j],pArr[j+1])>0){pTmp=pArr[j];pArr[j]=pArr[j+1];pArr[j+1]=pTmp;}}}printf("----------------------------\n");printPointArr(pArr);printf("----------------------------\n");system("pause");}
主函数中定义二级指针一般用于动态赋一级指针地址
二级指针初始化必须是某个一级指针变量取地址
5.5函数指针
由于本书给大家指导的路线是学完以后,学习Linux系统编程和C++,走C++后台开发工程师路线,因此函数指针并不重要,因为C++的重载使用起来更加方便,如果走嵌入式路线,那么学完函数指针,在Linux系统编程阶段深入研究一下回调函数,对于嵌入式,函数指针要熟练掌握。下面我们来看一个实例,定义函数指针p,将函数b赋值给p,为什么可以赋值呢,其实是因为函数名本身存储的即为函数入口地址,将p传递给函数a,相当于把一个行为传递给函数a,之前我们传递给函数的都是数据,通过函数指针可以将行为传递给一个函数,这样我们调用函数a可以就可以执行函数 b 的行为,当然也可以实现执行其他函数的行为。
#include<stdio.h>#include<stdlib.h>void b(){printf("I am b\n");}void a(void (*p)())//面向接口编程{p();}int main(){void (*p)();a(b);system("pause");}
第6章函数
6.1函数声明、定义与调用
6.1.1函数的声明与定义
6.1.2函数的分类与调用
6.2嵌套调用
6.3递归调用
6.4变量及函数的作用域
6.4.1局部变量与全局变量
6.4.2动态存储方式与静态存储方式
6.5函数调用原理详解
6.5.1关于栈
6.5.2代码实例分析
第7章结构体
7.1结构体与结构体指针
7.1.1结构体的定义、引用、初始化
7.1.2结构体指针
7.1.3typedef的使用
7.2链表的增删查改
7.2.1链表
7.2.2增删查改链表
7.3共用体与枚举
7.3.1共用体
7.3.2枚举
作业
1.打印菱形
打印原理:先行后列
0行 4空格 1(星号在空格后的位置) 2
1行 3空格 3 24
2行 2空格 5 246
3行 1空格 7
4行 0空格 9
5行 1空格
#include<stdio.h>#include<stdlib.h>int main(){int i,j;for(i=0;i<9;i++){for(j=0;j<abs(4-i);j++){putchar(' ');}for(j=0;j<9-2*abs(4-i);j++){if(j%2){putchar(' ');}else{putchar('*');}}putchar('\n');}system("pause");}
2.柱形图打印
无下标版
#include<stdio.h>#include<stdlib.h>#include<string.h>int main(){char c;int alp,num,oth;int max1,max2,max3,i;alp=num=oth=0;while(scanf("%c",&c)!=EOF){if(c>='A'&&c<='Z'||c>='a'&&c<='z'){alp++;}else if(c>='0'&&c<='9'){num++;}else if(c!='\n'){oth++;}}printf("alp=%d,num=%d,oth=%d\n",alp,num,oth);max1=alp>num?(alp>oth?alp:oth):(num>oth?num:oth);max3=alp<num?(alp<oth?alp:oth):(num<oth?num:oth);max2=alp+num+oth-max1-max3;for(i=0;i<=max1;i++){if(i==0){printf("%3d ",max1);}else{printf("*****");}if(i==max1-max2){printf("%3d ",max2);}else if(i>max1-max2){printf("*****");}if(i==max1-max3){printf("%3d ",max3);}else if(i>max1-max3){printf("*****");}printf("\n");}system("pause");}
冒泡排序版
#include<stdio.h>#include<stdlib.h>#include<string.h>int main(){char c;int arr[3]={0};char *p[3]={"alp","num","oth"},*pTmp;int i,j,tmp;while(scanf("%c",&c)!=EOF){if(c>='A'&&c<='Z'||c>='a'&&c<='z'){arr[0]++;}else if(c>='0'&&c<='9'){arr[1]++;}else if(c!='\n'){arr[2]++;}}printf("arr[0]=%d,arr[1]=%d,arr[2]=%d\n",arr[0],arr[1],arr[2]);//排序时同步交换//冒泡排序for(i=3;i>1;i--)//无序数的数目{for(j=0;j<i-1;j++)//内层控制比较{if(arr[j]<arr[j+1]){tmp=arr[j];arr[j]=arr[j+1];arr[j+1]=tmp;pTmp=p[j];p[j]=p[j+1];p[j+1]=pTmp;}}}for(i=0;i<=arr[0];i++){if(i==0){printf("%3d ",arr[0]);}else{printf("***** ");}if(i==arr[0]-arr[1]){printf("%3d ",arr[1]);}else if(i>arr[0]-arr[1]){printf("***** ");}if(i==arr[0]-arr[2]){printf("%3d ",arr[2]);}else if(i>arr[0]-arr[2]){printf("***** ");}printf("\n");}printf("%4s %4s %4s\n",p[0],p[1],p[2]);system("pause");}
3.分堆找只出现1次的数
2个出现1次的数
#include<stdio.h>#include<stdlib.h>#include<string.h>#define N 8int main(){int arr[N]={8,11,20,7,11,20,9,8};int split;//所有的数与split按位与int find1,find2,i,result=0;find1=find2=0;for(i=0;i<N;i++){result^=arr[i];//相同的数异或为0}printf("result=%d\n",result);split=result&-result;//找一个数最低位为1,与负数(取反+1)按位与for(i=0;i<N;i++){if(split&arr[i]){printf("heap1=%d\n",arr[i]);find1^=arr[i];}else{printf("heap2=%d\n",arr[i]);find2^=arr[i];}}printf("find1=%d,find2=%d\n",find1,find2);system("pause");}
3个出现1次的数
#include<stdio.h>#include<stdlib.h>#include<string.h>#define N 9void findTwoNum(int *arr,int result,int first){int split;//所有的数与split按位与int find1=0,find2=0,i;split=result&-result;//找一个数最低位为1,与负数(取反+1)按位与for(i=0;i<N;i++){if(split&arr[i]){find1^=arr[i];}else{find2^=arr[i];}}if(split%first){find1^=first;}else{find2^=first;}printf("find2=%d,find3=%d\n",find1,find2);}int main(){int arr[N]={8,11,20,7,11,20,9,8,5};int split;//所有的数与split按位与int heapResult1,heapResult2,i,j;int heapCount1=0,heapCount2=0;//每一堆的数目heapResult1=heapResult2=0;for(j=0;j<32;j++){split=1<<j;heapResult1=heapResult2=heapCount1=heapCount2=0;for(i=0;i<N;i++){if(split&arr[i])//按位与,真为奇数,假为偶数{heapCount1++;//奇数堆heapResult1^=arr[i];}else{heapCount2++;//偶数堆heapResult2^=arr[i];}}if(heapCount1%2==0&&heapResult1!=0)//奇数堆未分开需细分{printf("find1=%d\n",heapResult2);findTwoNum(arr,heapResult1,heapResult2);break;}if(heapCount2%2==0&&heapResult2!=0)//偶数堆未分开需细分{printf("find1=%d\n",heapResult1);findTwoNum(arr,heapResult2,heapResult1);break;}}system("pause");}
4.单词反转
-
#include<stdio.h>#include<stdlib.h>#include<string.h>void wordReverse(char *start,char *end){char tmp;while(start<end){tmp=*start;*start=*end;*end=tmp;start++;end--;}}int main(){char str[1000];char *front,*back;while(gets(str)!=NULL){wordReverse(str,str+strlen(str)-1);front=back=str;while(*front){while(*front==' '){front++;}back=front;while(*front!=' '&&*front){front++;}wordReverse(back,front-1);}puts(str);}system("pause");}
