关于C的杂货铺

补码

补码很简单,但是很重要。通过对补码的学习须知以下问题:

  • int型变量所能存储的数字的范围是多少?
  • 最小负整数的二进制代码是多少?
  • 最大正整数的二进制代码是多少?
  • 数字超过最大正整数会怎样?
  • int型变量和char型变量是如何相互赋值的?

原码和反码

原码:最高位0表示正,1表示负,其余二进制数是该数字绝对值的二进制数。

eg: -5的绝对值是5,5的二进制是0101,所以-5的原码就是0101

但是在计算机中-5不是这么存储的,这也是讲补码的原因。
计算机中所有的整数都是以补码的形式存储的。原码虽然简单、易懂,但因为存在加、减、乘、除四种运算,且加减运算复杂,使用原码增加了CPU的复杂度,使CPU设计起来比较困难。而且如果用原码的话,0的表示不唯一,可以是正0,也可以是负0,你可以写成100000,也可以写成000000。
而补码不一样,使用补码则减法、乘法都可以用加法进行处理,所以用补码比用原码好。

反码:在原码的基础上,符号位不变,其余各位取反。

补码:正数的补码就是其本身,负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。

注意: 不管是原码还是补码,都是最高位如果是0则表示正数,是1则表示负数。

各类型数据之间的混合运算

当数据类型不同时,先进行类型转换。
转换的方法有两种:

  • 自动转换:当不同类型的数据进行混合运算时,编译系统将按照一定的规则自动完成。
  • 强制转换:由程序员通过编程强制转换数据的类型。

自动转换的规则

  • 向数据长度增加的方向转换,以保证精度不降低。
  • 所有浮点运算都是以双精度进行的,因此,在运算时,程序中所有float型都会转换为double型。
  • char型和short型数据参与运算时,必须先转换为int型。
  • 有符号整型和无符号整型混合运算时,有符号型要转换程无符号型,运算的结果是无符号的。
// 对于有符号整型和无符号整型混合运算的示例:
int main()
{
	int a = -10;
	unsigned b = 5;
	cout << a + b << endl;
	if ((a + b) > 0) {
		printf("hello!\n");
	}
	return 0;
}

//cmd: hello!
  • 整型和浮点数混合运算时,整型先转换为浮点型,运算的结果时浮点型。
  • 在赋值运算中,当赋值号两边的数据类型不同时,右边的类型会转换为左边的类型,然后再赋给左边。如果右边的数据类型的长度比左边长,那么将会丢失数据,这样就会降低精度。

printf的用法

输出控制符
常用的输出控制符主要有以下几个:

  • %d:按十进制整型数据的实际长度输出。
  • %ld:输出长整型数据。
  • %md:m为指定的输出字段的宽度。如果数据的位数小于m,则左端补以空格,若大于m,则按实际输出。
  • %u:输出无符号整型(unsigned)。
  • %c:用来输出一个字符。
  • %f:用来输出实数,包括单精度和双精度,以小数形式输出。如果不指定字段宽度,由系统自动指定,整数部分全部输出,小数部分输出6位,超过6位的四舍五入。
  • %.mf:输出实数时小数点后保留m位,注意m前面有个点。
  • %o:以八进制整数形式输出。
  • %s:用来输出字符串。
  • %x(或%X或%#x或%#X):以十六进制输出整数。

%x、%X、%#x、%#X的区别
一定要掌握%x(或%X或%#x或%#X),因为调试的时候经常要将内存中的二进制代码全部输出,然后用十六进制显示出来。

// %x、%X、%#x、%#X的区别
int main()
{
	int i = 47;
	printf("%x\n", i);
	printf("%X\n", i);
	printf("%#x\n", i);
	printf("%#X\n", i);
	return 0;
}

//cmd:
2f
2F
0x2f
0X2F

如何输出“%d”、“\”和双引号
要输出“%d”只需在前面再加上一个“%”;要输出“\”只需在前面再加上一个“\”;要输出双引号也只需在前面加上一个“\”即可。
示例代码:

int main()
{
	printf("%%d\n");
	printf("\\\n");
	printf("\"\"\n");
    printf("%%\n");
	return 0;
}

// cmd:
%d
\
""
%

scanf的注意事项

  • 当scanf进入缓冲区中取数据时,如果%d遇到空格、回车、Tab键,那么它并不取用,而是跳过继续往过后取后面的数据。
  • 如果%d遇到字母,那么它不会跳过也不会取用,而是直接从缓冲区跳出。
  • 如果将%d换成%c,那么任何数据都会被当作一个字符,不管是数字还是空格、会回车、Tab键它都会取回。

运算符和表达式

算术运算符

  • +:加
  • -:减
  • *:乘
  • /:除:如果两个数都是int型,那么商就是int型,若商有小数,则舍去小数部分。两个数中只要有一个是浮点型数据,那么结果就是浮点型,不舍去小数部分。
  • %:取余
    • 取余“%”的运 算对象必须是整数,运算结果是整除后的余数。
    • 运算结果的符号与被除数的符号相同。只看被除数的符号,不看除数的符号。
  • ++:自增
  • –:自减

选择结构程序设计

当嵌套的if比较少时(三个以内),用if编写程序会比较简洁。但是当选择的分支比较多时,嵌套的if语句层数就会很多,导致程序冗余长,可读性下降。
switch语句

  • switch后面的括号内的“表达式”必须是整数类型。
  • switch下的case和default必须用一对大括号{}括起来。
  • 当switch后面括号内“表达式”的值与某个case后面的“常量表达式”的值相等时,就执行此case后面的语句。执行完一个case后面的语句后,流程控制转移到下一个case继续执行。如果你只想执行这一个case语句,不想执行其他case,那么就需要在这个case语句后面加上break,跳出switch语句。
  • 若所有的case中的常量表达式的值都没有与switch后面括号内“表达式”的值相等的,就执行default后面的语句,default是“默认”的意思。
  • 每个case后面“常量表达式”的值必须互不相同,否则就会出现互相矛盾的现象。

循环控制

for循环

for(表达式1; 表达式2; 表达式3){
    语句;
}

执行过程:

  1. 求解表达式1。
  2. 求解表达式2。若其值为真,则执行for语句中指定的内嵌语句,然后执行第3步;若表达式2值为假,则结束循环,转到第5步。
  3. 求解表达式3。
  4. 转回上面第2步继续执行。
  5. 循环结束,执行for语句下面的语句。

浮点数的存储所带来的问题

int main()
{
    float i = 5.4;
    float j = 3.235;
    printf("i = %.20f\n", i);
    printf("j = %.20f\n", j);
    return 0;
}

//cmd:
i = 5.40000009536743164062
j = 3.23499989509582519531

以上结果的原因是:
因为浮点型数据是以IEEE 754标准来存储的,以这种方式存储会导致无法十分准确地存储一个实数。

但是并不是所有的实数都不能准确地存储。小数部分是0的实数就可以准确地存储,如:0.0、1.0、2.0;有些小数部分不为0的实数也可以准确地存储,如0.5、1.5。

do…while
do…while用的不多,主要用于人机交互。

do
{
	语句;
}while(表达式);

注意:while后面的分号千万不能省略。

do…while和while的执行过程非常相似,唯一的区别是:do…while是先执行一次循环体,然后再判别表达式。

问题: do…while和while是否等价?是否可以相互转换?
不等价,不能相互转换。


清空输入缓冲区
所有从键盘输入的数据,不管是字符还是数字,都是先存储在内存中的一个缓冲池,叫作“键盘输入缓冲区”,简称“输入缓冲区”或“输入流”。

int main()
{
	int a;
	char i;
	while (1){
		printf("please enter a number: ");
		scanf_s("%d", &a);
		printf("a = %d\n", a);
		printf("do you want to continue?(Y/N): ");
		scanf_s("%c", &i);
		if ('Y' == i){
			printf("this is good!");
		}
		else {
			break;
		}
	}
	return 0;
}

// cmd:
please enter a number: 10 Y
a = 10
do you want to continue?(Y/N):

可以发现,当输入10 Y时,程序并未输出“this is good!”,为啥?
因为,scanf是从输入流中读取字符的,第一句scanf以%d读取时,遇到空格、回车、Tab键就会跳过。第二句scanf以%c读取时,遇到空格、回车、Tab键时,会将其当作数据读取。为避免此问题,可以使用getchar()函数。getchar()函数是专门从缓冲区读取一个字符的函数。

注意: 如果前面有多个scanf给int型变量赋值,那么每个scanf都会遗留一个回车,那这时是不是有几个scanf就要用几个getchar()呢?
不需要,仍然只需要一个getchar()!因为在缓冲区中,被跳过或读取过的字符都会被释放。所以假如前面有三个scanf给int型变量赋值,那么第一个scanf输入回车后把回车遗留在了缓冲区,而第二个scanf取值时会越过第一个scanf遗留在缓冲区中的回车,那么这个回车就会从缓冲区中释放。

建议: 为了避免忘记吸收回车或耗费精力考虑回车的问题,习惯上scanf后面都会加上getchar()。


fflush(stdin):清空输入缓冲区
一般用于清除用户前面遗留的垃圾数据,提高代码的健壮性。

宏定义:#define

在从语言中可以用#define定义一个标识符常量。
定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了。预编译又叫预处理。预编译不是编译,而是编译前的处理。这个操作是在正式编译之前由系统自动完成的,所以叫预编译。

#define 标识符 常量

//宏所表示的常量可以是数字、字符、字符串、表达式。

#define又称宏定义,标识符为所定义的宏名,简称宏。

#define的作用域为自#define那一行起到源程序结束。如果要终止其作用域可以使用#undef命令,格式为:

#undef 标识符

变量的作用域和存储方式

变量按作用域可分为“局部变量”和“全局变量”。
按存储方式又可分为“自动变量(auto)”、“静态变量(static)”、“寄存器变量(register)”和“外部变量(extern)”。

自动变量(auto)
前面定义的局部变量其实都是auto型,只不过auto可以省略,因为省略就默认是auto型。

静态变量(static)
用static修饰过的局部变量称为静态局部变量。局部变量如果不用static进行修饰,那么默认都是auto型,即存储在栈区。

注意:存储在静态存储区中的变量如果未初始化,系统会自动将其初始化为0。

静态存储区主要用于存放静态数据和全局数据。

寄存器变量(register)
为提高代码的执行效率,可将频繁使用的变量存储到寄存器中。

register int a;

用程序用到哪个变量时,CPU的控制器就会发出指令将该变量的值从内存读到CPU里面。然后CPU再对它进行处理,处理完了再把结果写回内存。

但是处理内存之外,CPU中的寄存器也可以用来存储数据。

计算机中硬件运行速度由快到慢的顺序是:
寄存器 > 缓存 > 内存 > 固态硬盘 > 机械硬盘

寄存器变量是一个局部变量,在函数调用结束后就会被释放。

register目前已经不再使用,原因是现在的编译器会自动分析变量的使用频率,如果变量使用频率过高,即使定义的是auto类型,也会转换为register类型。

外部变量(extern)
一般而言,外部变量是在函数的外部定义的全局变量,它的作用域是从变量的定义处开始,到本程序文件的末尾结束。

扩展全局变量的作用域的几种情况:

  • 在一个文件内扩展全局变量的作用域
  • 将外部变量的作用域扩展到其他文件

指针

指针的优点

  1. 通过指针可以表示一些复杂的数据结构;
  2. 使用指针能够快速、高效地传递数据;
  3. 在调用函数时能使函数返回一个以上的结果;
  4. 指针能够直接访问硬件,可以直接操作地址;
  5. 指针可以非常方便地用来指向字符串,使字符串的处理更加灵活方便;
  6. 指针是理解面向对象语言中“引用”的基础;
  7. 很多知识实际上内部都需要指针的知识。

关于const 与 指针的奇妙关系请访问here:const and ptr

注意:用const修饰的变量,无论是全局变量还是局部变量,生存周期都是程序运行的整个过程。经过const修饰过的变量存储在内存中的“只读数据段”中。只读数据段中存放着常量和只读变量等不可修改的量。

const与define的区别:

  • define是预编译指令,而const是普通变量的定义。define定义的宏实在预处理阶段展开的,而const定义的只读变量在编译运行阶段使用的。
  • const定义的是变量,而define定义的是常量。define定义的宏在编译后就不存在了,它不占用内存,因为它不是变量,系统只会给变量分配内存。
  • const定义的是变量,而宏定义的是常量,所以const定义的对象有数据类型,而宏定义的对象没有数据类型。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值