C语言学习笔记(基础语法篇)
C语言学习笔记(基础语法篇)
序言
首先事先说明一下,这是我从各处整理的,当初刚接触CS,甚至连标注意识都没有,再次感谢写这些文章的人.当然这里不是说全部都是别人写的了,也有一点我自己的思考.
首先是几个注意点:
-
结构化,模块化,分而治之
-
多写注释,多调试
-
指针也有不同类型
-
在 C 语言中,所有的变量命名都必须遵循某些语法规则,才能算是有效的。一些主要要点:
- 变量名可以包含字母、数字和下划线。
- 变量名的首字母必须是字母或下划线。
- C 语言是大小写敏感的–
myVar
和myvar
是不同的变量。 - 变量名不能包含空格或特殊字符,如
&
、*
、$
等。 - 名字不能与 C 语言关键字冲突,如
if
、else
、int
等。
-
隐形类型转化要注意,是根据要求的数的类型.
-
'' 引起的一个字符代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。
"" 引起的字符串代表的是一个指向无名数组起始字符的指针。
-
搞指针建议初始化为null指针,减少错误.
-
&&前面如果是0后面既不会判断,||前面如果是1后面就不会判断,所以无论后面有什么运算都不生效
-
++在右边先不参与计算,甚至是赋值,是最低等的
-
不要写10<=a<=100这种sb东西
-
数组必须用常量初始
-
结构体如果有指针要加括号
-
++是自增,也就是只有最开始的a会增加:b+a++,只有a会加1.
-
==
-
初始化
-
long long a = (long long)2147483647 +(long long)1;
main
主函数 有三个参数 int argc(参数个数,包括自己,比如1.exe 2.exe 3.exe就是三个) char* argv[]里面的每一个指针指向命令行的一个字符串.同时main不一定是第一个函数,在一些调试检测需求下,以下是有可能会出现的:(1)全局对象的构造函数会在main 函数之前执行。
(2)一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前,而main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作
(3)进程启动后,要执行一些初始化代码(如设置环境变量等),然后跳转到main执行。全局对象的构造也在main之前。
(4)通过关键字 __attribute__
,让一个函数在主函数之前运行,进行一些数据初始化、模块加载验证等。
常见转义字符
转义字符 释义
? 在书写连续多个问号时使用,防止他们被解析成三字母词
` 用于表示字符常量`
" 用于表示一个字符产内部的双引号
\ 用于表示一个反斜杠,防止它被解释为一个转义序列符
\a 警告字符,蜂鸣
\b 退格符
\f 进纸符
\n 换行
\r 回车
\t 水平制表符
\v 垂直制表符
\ddd ddd表示1~3个八进制的数字。如:\130X
\xdd dd表示2个十六进制数字。如:\x30 0
for(int i=0;i<n;i++)
printf("%d%c",a[i]," \n"[i==n-1]);
规定符
- %d 十进制有符号整数
- %ld 32位十进制有符号整数(位数取决于编译器)
- %lld 64位十进制有符号整数(位数取决于编译器)
- %u 十进制无符号整数
- %f 浮点数 //float
- %lf 长浮点型 //double
- %s 字符串
- %c 单个字符
- %p 指针的值
- %e 指数形式的浮点数
- %x, %X 无符号以十六进制表示的整数
- %o 无符号以八进制表示的整数
- %g 把输出的值按照 %e 或者 %f 类型中输出长度较小的方式输出
- %p 输出地址符
- %lu 32位无符号整数(位数取决于编译器)
- %llu 64位无符号整数(位数取决于编译器)
- %% 输出百分号字符本身。
除了格式化说明符之外,printf() 函数还支持一些标志和选项,用于控制输出的精度、宽度、填充字符和对齐方式等。例如:
-
%-10s:左对齐并占用宽度为 10 的字符串;
-
%5.2f:右对齐并占用宽度为 5,保留两位小数的浮点数;
-
%#x:输出带有 0x 前缀的十六进制数。
-
%0d:用0补位
-
% * (整型)如%* d,%-*(整型):在输出项中规定整型数据的宽度,少于限制补空格,大于忽略
如printf(“%*d”,a,b); a=5 b=123 结果为 123(前面有两个空格)
scanf()也有:
- % * * (所有类型),如%* d 用来输入一个数,字符或字符串而不赋值(跳过无关输入)如scanf("%d%*c%d",&a,&b);这样就可以只将1+2中的1和2赋值给a和b。
- %m(所有类型),其中m为常数 限定输入范围,如scanf(“%4d”,&a)时输入123456,只把1234赋值给a
数据类型
在 C 语言中,数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间,以及如何解释存储的位模式。
C 中的类型可分为以下几种:
序号 | 类型与描述 |
---|---|
1 | 基本数据类型 它们是算术类型,包括整型(int)、字符型(char)、浮点型(float)和双精度浮点型(double)。 |
2 | 枚举类型: 它们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值的变量。 |
3 | void 类型: 类型说明符 void 表示没有值的数据类型,通常用于函数返回值。 |
4 | 派生类型: 包括数组类型、指针类型和结构体类型。 |
数组类型和结构类型统称为聚合类型。函数的类型指的是函数返回值的类型。在本章节接下来的部分我们将介绍基本类型,其他几种类型会在后边几个章节中进行讲解。
整数类型
下表列出了关于标准整数类型的存储大小和值范围的细节(注意这是编译器的设置,c语言一个字节不一定是八位,因为标准没有明说):
类型 | 存储大小 | 值范围 |
---|---|---|
char | 1 字节 | -128 到 127 或 0 到 255 |
unsigned char | 1 字节 | 0 到 255 |
signed char | 1 字节 | -128 到 127 |
int | 2 或 4 字节 | -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647(突然发现int不是ascii码,是直接用二进制表示) |
unsigned int | 2 或 4 字节 | 0 到 65,535 或 0 到 4,294,967,295 |
short | 2 字节 | -32,768 到 32,767 |
unsigned short | 2 字节 | 0 到 65,535 |
long | 4 字节 | -2,147,483,648 到 2,147,483,647 |
unsigned long | 4 字节 | 0 到 4,294,967,295 |
注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主。
以下列出了32位系统与64位系统的存储大小的差别(windows 相同):
为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用 sizeof 运算符。表达式 sizeof(type) 得到对象或类型的存储字节大小。下面的实例演示了获取 int 类型的大小:
实例
#include <stdio.h> #include <limits.h> int main() { printf("int 存储大小 : %lu \n", sizeof(int)); return 0; }
%lu 为 32 位无符号整数,详细说明查看 C 库函数 - printf()。
当您在 Linux 上编译并执行上面的程序时,它会产生下列结果:
int 存储大小 : 4
浮点类型
下表列出了关于标准浮点类型的存储大小、值范围和精度的细节:
类型 | 存储大小 | 值范围 | 精度 |
---|---|---|---|
float | 4 字节 | 1.2E-38 到 3.4E+38 | 6 位有效位 |
double | 8 字节 | 2.3E-308 到 1.7E+308 | 15 位有效位 |
long double | 16 字节 | 3.4E-4932 到 1.1E+4932 | 19 位有效位 |
ps:
some = 4.0 * 2.0;
通常,4.0和2.0被储存为64位的double类型,使用双精度进行乘法运算,然后将乘积截断成float类型的宽度。这样做虽然计算精度更高,但是会减慢程序的运行速度。
在浮点数后面加上f或F后缀可覆盖默认设置,编译器会将浮点型常量看作float类型,如2.3f和9.11E9F。使用l或L后缀使得数字成为long double类型,如54.3l和4.32L。注意,建议使用L后缀,因为字母l和数字1很容易混淆。没有后缀的浮点型常量是double类型。
头文件 float.h 定义了宏,在程序中可以使用这些值和其他有关实数二进制表示的细节。下面的实例将输出浮点类型占用的存储空间以及它的范围值:
实例
#include <stdio.h> #include <float.h> int main() { printf("float 存储最大字节数 : %lu \n", sizeof(float)); printf("float 最小值: %E\n", FLT_MIN ); printf("float 最大值: %E\n", FLT_MAX ); printf("精度值: %d\n", FLT_DIG ); return 0; }
%E 为以指数形式输出单、双精度实数,详细说明查看 C 库函数 - printf()。
当您在 Linux 上编译并执行上面的程序时,它会产生下列结果:
float 存储最大字节数 : 4
float 最小值: 1.175494E-38
float 最大值: 3.402823E+38
精度值: 6
void 类型
void 类型指定没有可用的值。它通常用于以下三种情况下:
序号 | 类型与描述 |
---|---|
1 | 函数返回为空 C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status); |
2 | 函数参数为空 C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void); |
3 | 指针指向 void 类型为 void * 的指针代表对象的地址,而不是类型。例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。 |
如果现在您还是无法完全理解 void 类型,不用太担心,在后续的章节中我们将会详细讲解这些概念。
类型转换
类型转换是将一个数据类型的值转换为另一种数据类型的值。
C 语言中有两种类型转换:
- 隐式类型转换:隐式类型转换是在表达式中自动发生的,无需进行任何明确的指令或函数调用。它通常是将一种较小的类型自动转换为较大的类型,例如,将int类型转换为long类型或float类型转换为double类型。隐式类型转换也可能会导致数据精度丢失或数据截断。
- 显式类型转换:显式类型转换需要使用强制类型转换运算符(type casting operator),它可以将一个数据类型的值强制转换为另一种数据类型的值。强制类型转换可以使程序员在必要时对数据类型进行更精确的控制,但也可能会导致数据丢失或截断。
隐式类型转换实例:
实例
int i = 10;
float f = 3.14;
double d = i + f; // 隐式将int类型转换为double类型
显式类型转换实例:
实例
double d = 3.14159;
int i = (int)d; // 显式将double类型转换为int类型
注意:int a = 8, b = 5, c;
c = a/b+ 0.4;
c=1
反码和补码
一. 机器数和机器数的真值
在学习原码,反码和补码之前, 需要先了解机器数和真值的概念。
1、机器数
一个数在计算机中的二进制表示形式,叫做这个数的机器数。机器数是带符号的,在计算机用机器数的最高位存放符号,正数为0,负数为1。
比如,十进制中的数 +3 ,计算机字长为8位,转换成二进制就是0000 0011。如果是 -3 ,就是 100 00011 。
那么,这里的 0000 0011 和 1000 0011 就是机器数。
2、机器数的真值
因为第一位是符号位,所以机器数的形式值就不等于真正的数值。
例如上面的有符号数 1000 0011,其最高位1代表负,其真正数值是 -3,而不是形式值131(1000 0011转换成十进制等于131)。所以,为区别起见,将带符号位的机器数对应的真正数值称为机器数的真值。
例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1
二. 原码, 反码, 补码的基础概念和计算方法
在探求为何机器要使用补码之前,让我们先了解原码、反码和补码的概念。对于一个数,计算机要使用一定的编码方式进行存储,原码、反码、补码是机器存储一个具体数字的编码方式。
1.原码
原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值。比如:如果是8位二进制:
[+1]原= 0000 0001
[-1]原= 1000 0001
第一位是符号位,因为第一位是符号位,所以8位二进制数的取值范围就是:(即第一位不表示值,只表示正负。)
[1111 1111 , 0111 1111]
即
[-127 , 127]
原码是人脑最容易理解和计算的表示方式。
2. 反码
反码的表示方法是:
正数的反码是其本身;
负数的反码是在其原码的基础上,符号位不变,其余各个位取反。
[+1] = [0000 0001]原= [0000 0001]反
[-1] = [1000 0001]原= [1111 1110]反
可见如果一个反码表示的是负数,人脑无法直观的看出来它的数值。通常要将其转换成原码再计算。
3. 补码
补码的表示方法是:
正数的补码就是其本身;
负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后+1。(也即在反码的基础上+1)
[+1] = [0000 0001]原= [0000 0001]反= [0000 0001]补
[-1] = [1000 0001]原= [1111 1110]反= [1111 1111]补
对于负数,补码表示方式也是人脑无法直观看出其数值的。通常也需要转换成原码再计算其数值。
三. 为何要使用原码、反码和补码
在开始深入学习前,我的学习建议是先"死记硬背"上面的原码,反码和补码的表示方式以及计算方法。
现在我们知道了计算机可以有三种编码方式表示一个数,对于正数因为三种编码方式的结果都相同:
[+1] = [0000 0001]原= [0000 0001]反= [0000 0001]补
所以不需要过多解释,但是对于负数:
[-1] = [10000001]原= [11111110]反= [11111111]补
可见原码,反码和补码是完全不同的。既然原码才是被人脑直接识别并用于计算表示方式,为何还会有反码和补码呢?
首先, 因为人脑可以知道第一位是符号位,在计算的时候我们会根据符号位,选择对真值区域的加减。(真值的概念在本文最开头) 但是对于计算机,加减乘数已经是最基础的运算,要设计的尽量简单,计算机辨别"符号位"显然会让计算机的基础电路设计变得十分复杂!
于是人们想出了将符号位也参与运算的方法。我们知道,根据运算法则减去一个正数等于加上一个负数,即:1-1 = 1 + (-1) = 0, 所以机器可以只有加法而没有减法,这样计算机运算的设计就更简单了。
于是人们开始探索将符号位参与运算,并且只保留加法的方法。
首先来看原码:
计算十进制的表达式: 1 - 1 = 0
1 - 1 = 1 + (-1) = [0000 0001]原+ [1000 0001]原= [1000 0010]原= -2
如果用原码表示,让符号位也参与计算,显然对于减法来说,结果是不正确的。这也就是为何计算机内部不使用原码表示一个数。
为了解决原码做减法的问题, 出现了反码:
计算十进制的表达式:1 - 1 = 0
1 - 1 = 1 + (-1) = [0000 0001]原+ [1000 0001]原= [0000 0001]反+ [1111 1110]反= [1111 1111]反= [1000 0000]原= -0
发现用反码计算减法,结果的真值部分是正确的。而唯一的问题其实就出现在"0"这个特殊的数值上,虽然人们理解上+0和-0是一样的,但是0带符号是没有任何意义的,而且会有[0000 0000]原和[1000 0000]原两个编码表示0。
于是补码的出现,解决了0的符号问题以及0的两个编码问题:
1-1 = 1 + (-1) = [0000 0001]原+ [1000 0001]原= [0000 0001]补+ [1111 1111]补= [1 0000 0000]补=[0000 0000]补=[0000 0000]原注意:进位1不在计算机字长里。
这样0用[0000 0000]表示,而以前出现问题的-0则不存在了。而且可以用[1000 0000]表示-128:-128的由来如下:
(-1) + (-127) = [1000 0001]原+ [1111 1111]原= [1111 1111]补+ [1000 0001]补= [1000 0000]补
-1-127的结果应该是-128,在用补码运算的结果中,[1000 0000]补就是-128,但是注意因为实际上是使用以前的-0的补码来表示-128,所以-128并没有原码和反码表示。(对-128的补码表示[1000 0000]补,算出来的原码是[0000 0000]原,这是不正确的)
使用补码,不仅仅修复了0的符号以及存在两个编码的问题,而且还能够多表示一个最低数。这就是为什么8位二进制,使用原码或反码表示的范围为[-127, +127],而使用补码表示的范围为[-128, 127]。
因为机器使用补码,所以对于编程中常用到的有符号的32位int类型,可以表示范围是: [-231, 231-1] 因为第一位表示的是符号位,而使用补码表示时又可以多保存一个最小值。
(其实,这是反着推导出来的结果,搞得很神奇一样。实际上,只要基于一个最基本的数学知识 x + (-x) = 0,就能推导出计算机体系里面的负数是多少。计算机里面实际上是没有减法和负数的,那么两个不为0的数相加如何能等于0,只有溢出一条路。所以,5 和 哪个数 相加会刚好溢出呢?就是 0b1111 1011,所以就用这个数来表示 -5。)评论区的评论
四.取反的原理
就拿0举例
0 为 0000 0000,取反为 1111 1111,这时首字母为1,为补码形式,而补码反码一次即为原码,则其原码为 1000 0001,即-1,故0取反为-1.
输入方式
-
一种就是for(i=0;(c[i]=getchar())!=xxxx;xxx);但是这个要在末尾加\0(但一般都有按回车所以可以直接把'\n'改为'\0')
-
另一种就是scanf("%s",c) //输入时不用&而是直接写数组名,自动加'\0',输入长度<数组长度(要留一位放'\0'),遇到空格,tab,回车结束.scanf()返回值为输入数据的个数,有一个就返回一个,两个就两个,如果没有就返回eof.可利用这个与while()组合使用.
-
gets()
最快,因为它本质上是宏,是替换,虽然代码较大字面意思,读取多个字符,实际上是读取一整行,使用方法
char str[80]; gets(str);
实际效果等同于:
char str[80]; scanf("%[^n]",str);
由于gets()不检查字符串string的大小,必须遇到换行符或文件结尾才会结束输入,因此容易造成缓存溢出的安全性问题,导致程序崩溃,可以使用
fgets()
代替。
另外,有的时候代码中可能会出现getline()
方法,虽然格式可能相同,但实际上这是c++的输入方法。fgets()
是对
gets()
方法的扩展,gets()
是从标准输入流中读取,而fgets()
是从文件输入流中读取,但是文件输入流并不局限于普通的文件,只要是流都可以用来输入,使用方法:char str[80]; fgets(str,79,stdin);
方法与
gets()
相比,多添加了两个参数,第二个参数限定要读取的最大长度,最终读取的长度不超过还未读取的剩余行长度;第三个参数说明从哪个流读取输入,通过定义stdin
,我们就可以定义从标准输入中读取。注意:
fgets()
方法接受到行尾时会接收换行符!,这一点非常特殊,一定要注意。getchar和putchar(比printf更快):一个是输入(字符),一个是输出(字符).(两者都是以ascii的形式)注意:如果中间有"空格"则空格会当成一个字符,下例:
(若只有三个getchar和三个putchar)输入:"abc"则输出"abc",输入"a b c",输出"a b".
getchar()到结尾或故障返回EOF,注意一点,比如前面用scanf()那肯定会有回车在缓冲区,那么getchar就会读取这个回车会多一个回车,可以用while(getchar()!='\n'){}来清理.接受空格
输入技巧
上面展示的方法基本上足够应用大部分场景,但是有的时候,用一些特殊的方法,能让我们更有效率的接受字符串,我总结了以下几个
限制每次读入的字符串长度
在百分号(%)与格式码之间添加一个整数可以限制读入的最大字符数,超出字符串的部份将留在缓冲区等待下次读取。
例如:向变量 A
读入不多于 20 个字符时的代码:
char A[20];
scanf("%20s",&A);
注意读入字符串需要注意数组长度的设置,上面的例子实际上是不严谨的,因为读取到结束时候虽然会忽略空白符,但是会添加"\0"用来标识结束,如果刚好填满数组的话,会导致内存溢出,从而可能出现一些未知错误,正确的写法应该如下(假设需要读入最长20个字符的字符串):
char A[21];
scanf("%20s",&A);
最后,这种方式不仅仅局限于输入字符串,限制的长度只是限制了该方法一次性能从缓冲区看到的字符串长度,也就是说,还可以用于接收整数,浮点数等
int a;
scanf("%2d",&a);
/**
* 输入"12345",运行后 a=12
*/
读入字符但是忽略
scanf( "%d%*c%d", &x, &y );
/**
* 输入 "10/20"
* 10放入变量x,20放入变量y,'/'被接受但是被忽略
* 这种方式可以用来匹配中间分隔符未知的情况
*/
判断行尾
一般算法题中,根据输入一般是能确定输入中每一行的长度(或者要读取多少次),但是仍然有一些题没有明确的给出,需要手动判断或后期处理,简单举一个例子:
给N行数字,每一行由纯数字组成,保证每一行的数字个数为偶数个,按相邻的两个数字为一个数(不重叠),对每一行求和并输出
如:对于123456,被分为12+34+56=102
对于这个问题,单纯的读取连续的两个数字,按照上面的技巧,是很容易的,格式符为 %2d
,这个问题的主要难点是我们不好判断一行什么时候结束,如果单纯的使用 scanf()
方法,没有很好的解决方案,只能通过一个字符一个字符的读取然后再组装成数字。这样实际上白白浪费了时间和精力,有没有更好的方法?当然有,那就是使用方法 scanf()
scanf()
方法和 scanf()
方法基本一样,唯一不同的是其前面多了一个参数,传递进去的是char型数组,通过该方法,我们可以先用 gets()
方法读取一整行,用 strlen()
方法求出行长度,随后我们就可以用 sscanf()
方法来二次提取,核心代码如下:
char str[80];
gets(str);
int len = strlen(str);//需要引入cstring库或string.c
int sum = 0;
for(int i = 0;i<len/2;i++)
{
int num;
sscanf(str+i*2,"%2d",&num);
sum += num;
}
其中第一个参数传入的是char型数组(实际上传入的是指针,str
表示的是第1个元素所对应的位置,每加1就向下迭代一次,c里面字符串没有办法切片,但可以用这种方法更改字符串的起始位置)
1、while(scanf("%d",&n),n)
功能:当n为0时中止循环
这里要先说一下逗号表达式:逗号表达式的值是逗号后面的那个数。例如x=(5,6),则x=6。
while(scanf("%d",&n),n)
括号里的语句其实就是个逗号表达式,它的返回值是n的值,所以这个语句就相当于while(n),n=0时跳出循环,写成这样是为## 标题了输入。
如果是while(scanf("%d%d",&n,&m,),n,m),那么就相当于while(m)。
2、while(scanf("%d",&n)!=EOF)和while(~scanf("%d",&n))
功能:当读到文件结尾时中止循环
scanf语句的返回值为成功赋值的个数,例如scanf("%d %d",&a,&b),如果a、b均赋值成功返回值为2,只是a赋值成功返回1,a、b都不成功返回0,出错的时候返回EOF。
~是按位取反,scanf语句如果没有输入值就是返回-1,按位取反结果为0。
注意:这两种方法在输入字母的时候会变成死循环,而scanf("%d %d",&a,&b)==2不会。windows下可通过按“Ctrl +Z”、linux下可通过“Ctrl + D”来来达到“输入”文件结束符的效果,结束循环。
3、while(scanf("%d",&n)==1)
功能:赋值失败,跳出循环
这个应该很好理解了吧,如果是scanf("%d%d",&n,&m)就是while(scanf("%d %d",&a,&b)==2)
编译预处理
宏定义
define的宏定义只是==替换(一般来说,宏定义用大写字母,与普通变量区分),下例:
#include<stdio.h>
#define a 10
#define b 20+a
b*2=20+10*2 //要达到先加后乘的效果应该是把b定义为(20+a)
宏展开还能这样做: #define product(a,b) a*b //定义了这个函数是a×b 注意:由于宏定义在预编译中,是编译之前,没有检查语法.用#undef可以终止宏定义的作用域.
宏定义只是机械的替换,所以会出现这样的情况:#define s(x) x*x int main(){int x=5; printf("s(x+1)");}那会输出x+1 * x+x,所以最好每个值加上小括号避免这种情况
宏定义的技巧:在宏定义时,用#可以把后面的参数变成字符串,比如#define STR(s) # s那么这个函数就是把s变成字符串.这个中如果有多个空格会变成一个空格,双引号会有反斜杠.
使用##则是连接一起,比如#define STR(x,y) x##y,用这个函数的结果就是xy
声明
函数可以声明后摆在任意位置,其形式为:如果有一个函数定义为 void a(void){},其声明为 void a(void); (即加个分号)
存储类型
const int MONTHS = 12; // MONTHS在程序中不可更改,值为12
extern是一种“外部声明”的关键字,字面意思就是在此处声明某种变量或函数,在外部定义。比如:extern int a;
运算符与表达式
运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符,并提供了以下类型的运算符:
- 算术运算符
- 关系运算符
- 逻辑运算符
- 位运算符
- 赋值运算符
- 杂项运算符
本章将逐一介绍算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符和其他运算符。
算术运算符
下表显示了 C 语言支持的所有算术运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
+ | 把两个操作数相加 | A + B 将得到 30 |
- | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
* | 把两个操作数相乘 | A * B 将得到 200 |
/ | 分子除以分母 | B / A 将得到 2 |
% | 取模运算符,整除后的余数.ps:浮点型不能用,要用math.h的fmod()和fmodl() | B % A 将得到 0 |
++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
-- | 自减运算符,整数值减少 1 | A-- 将得到 9 |
实例
#include <stdio.h> int main() { int a = 21; int b = 10; int c ; c = a + b; printf("Line 1 - c 的值是 %d\n", c ); c = a - b; printf("Line 2 - c 的值是 %d\n", c ); c = a * b; printf("Line 3 - c 的值是 %d\n", c ); c = a / b; printf("Line 4 - c 的值是 %d\n", c ); c = a % b; printf("Line 5 - c 的值是 %d\n", c ); c = a++; // 赋值后再加 1 ,c 为 21,a 为 22 printf("Line 6 - c 的值是 %d\n", c ); c = a--; // 赋值后再减 1 ,c 为 22 ,a 为 21 printf("Line 7 - c 的值是 %d\n", c ); }
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - c 的值是 31
Line 2 - c 的值是 11
Line 3 - c 的值是 210
Line 4 - c 的值是 2
Line 5 - c 的值是 1
Line 6 - c 的值是 21
Line 7 - c 的值是 22
以下实例演示了 a++ 与 ++a 的区别(其实就是放前面先进行):
实例
#include <stdio.h> int main() { int c; int a = 10; c = a++; printf("先赋值后运算:\n"); printf("Line 1 - c 的值是 %d\n", c ); printf("Line 2 - a 的值是 %d\n", a ); a = 10; c = a--; printf("Line 3 - c 的值是 %d\n", c ); printf("Line 4 - a 的值是 %d\n", a ); printf("先运算后赋值:\n"); a = 10; c = ++a; printf("Line 5 - c 的值是 %d\n", c ); printf("Line 6 - a 的值是 %d\n", a ); a = 10; c = --a; printf("Line 7 - c 的值是 %d\n", c ); printf("Line 8 - a 的值是 %d\n", a ); }
以上程序执行输出结果为:
先赋值后运算:
Line 1 - c 的值是 10
Line 2 - a 的值是 11
Line 3 - c 的值是 10
Line 4 - a 的值是 9
先运算后赋值:
Line 5 - c 的值是 11
Line 6 - a 的值是 11
Line 7 - c 的值是 9
Line 8 - a 的值是 9
关系运算符
下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
!= | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
> | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
< | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
>= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
<= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
实例
#include <stdio.h> int main() { int a = 21; int b = 10; int c ; if( a == b ) { printf("Line 1 - a 等于 b\n" ); } else { printf("Line 1 - a 不等于 b\n" ); } if ( a < b ) { printf("Line 2 - a 小于 b\n" ); } else { printf("Line 2 - a 不小于 b\n" ); } if ( a > b ) { printf("Line 3 - a 大于 b\n" ); } else { printf("Line 3 - a 不大于 b\n" ); } /* 改变 a 和 b 的值 */ a = 5; b = 20; if ( a <= b ) { printf("Line 4 - a 小于或等于 b\n" ); } if ( b >= a ) { printf("Line 5 - b 大于或等于 a\n" ); } }
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - a 不等于 b
Line 2 - a 不小于 b
Line 3 - a 大于 b
Line 4 - a 小于或等于 b
Line 5 - b 大于或等于 a
逻辑运算符
下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:
运算符 | 描述 | 实例 |
---|---|---|
&& | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
|| | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A|| B) 为真。 |
! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
实例
#include <stdio.h> int main() { int a = 5; int b = 20; int c ; if ( a && b ) { printf("Line 1 - 条件为真\n" ); } if ( a || b ) { printf("Line 2 - 条件为真\n" ); } /* 改变 a 和 b 的值 */ a = 0; b = 10; if ( a && b ) { printf("Line 3 - 条件为真\n" ); } else { printf("Line 3 - 条件为假\n" ); } if ( !(a && b) ) { printf("Line 4 - 条件为真\n" ); } }
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - 条件为真
Line 2 - 条件为真
Line 3 - 条件为假
Line 4 - 条件为真
位运算符
位运算符作用于位,并逐位执行操作。&、 | 和 ^ 的真值表如下所示:
p | q | p & q | p| q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
~A = 1100 0011
下表显示了 C 语言支持的位运算符。假设变量 A 的值为 60,变量 B 的值为 13,则:
运算符 | 描述 | 实例 |
---|---|---|
& | 对两个操作数的每一位执行逻辑与操作,如果两个相应的位都为 1,则结果为 1,否则为 0。按位与操作,按二进制位进行"与"运算。运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1; |
(A & B) 将得到 12,即为 0000 1100 |
| | 对两个操作数的每一位执行逻辑或操作,如果两个相应的位都为 0,则结果为 0,否则为 1。按位或运算符,按二进制位进行"或"运算。运算规则:`0 | 0=0; 0 |
^ | 对两个操作数的每一位执行逻辑异或操作,如果两个相应的位值相同,则结果为 0,否则为 1。异或运算符,按二进制位进行"异或"运算。运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0; |
(A ^ B) 将得到 49,即为 0011 0001 |
~ | 对操作数的每一位执行逻辑取反操作,即将每一位的 0 变为 1,1 变为 0。取反运算符,按二进制位进行"取反"运算。运算规则:~1=-2; ~0=-1; |
(~A ) 将得到 -61,即为 1100 0011,一个有符号二进制数的补码形式。 |
<< | 将操作数的所有位向左移动指定的位数。左移 n 位相当于乘以 2 的 n 次方。二进制左移运算符。将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。 | A << 2 将得到 240,即为 1111 0000 |
>> | 将操作数的所有位向右移动指定的位数。右移n位相当于除以 2 的 n 次方。二进制右移运算符。将一个数的各二进制位全部右移若干位,正数左补 0,负数左补 1,右边丢弃。 | A >> 2 将得到 15,即为 0000 1111 |
实例
#include <stdio.h> int main() { unsigned int a = 60; /* 60 = 0011 1100 / unsigned int b = 13; / 13 = 0000 1101 / int c = 0; c = a & b; / 12 = 0000 1100 / printf("Line 1 - c 的值是 %d\n", c ); c = a | b; / 61 = 0011 1101 / printf("Line 2 - c 的值是 %d\n", c ); c = a ^ b; / 49 = 0011 0001 / printf("Line 3 - c 的值是 %d\n", c ); c = ~a; /-61 = 1100 0011 / printf("Line 4 - c 的值是 %d\n", c ); c = a << 2; / 240 = 1111 0000 / printf("Line 5 - c 的值是 %d\n", c ); c = a >> 2; / 15 = 0000 1111 */ printf("Line 6 - c 的值是 %d\n", c ); }
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - c 的值是 12
Line 2 - c 的值是 61
Line 3 - c 的值是 49
Line 4 - c 的值是 -61
Line 5 - c 的值是 240
Line 6 - c 的值是 15
赋值运算符
下表列出了 C 语言支持的赋值运算符:
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,把右边操作数的值赋给左边操作数 | C = A + B 将把 A + B 的值赋给 C |
+= | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
-= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
*= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
/= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
%= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
<<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
>>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
&= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
|= | 按位或且赋值运算符 | C|= 2 等同于 C = C | 2 |
实例
#include <stdio.h> int main() { int a = 21; int c ; c = a; printf("Line 1 - = 运算符实例,c 的值 = %d\n", c ); c += a; printf("Line 2 - += 运算符实例,c 的值 = %d\n", c ); c -= a; printf("Line 3 - -= 运算符实例,c 的值 = %d\n", c ); c *= a; printf("Line 4 - *= 运算符实例,c 的值 = %d\n", c ); c /= a; printf("Line 5 - /= 运算符实例,c 的值 = %d\n", c ); c = 200; c %= a; printf("Line 6 - %%= 运算符实例,c 的值 = %d\n", c ); c <<= 2; printf("Line 7 - <<= 运算符实例,c 的值 = %d\n", c ); c >>= 2; printf("Line 8 - >>= 运算符实例,c 的值 = %d\n", c ); c &= 2; printf("Line 9 - &= 运算符实例,c 的值 = %d\n", c ); c ^= 2; printf("Line 10 - ^= 运算符实例,c 的值 = %d\n", c ); c |= 2; printf("Line 11 - |= 运算符实例,c 的值 = %d\n", c ); }
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - = 运算符实例,c 的值 = 21
Line 2 - += 运算符实例,c 的值 = 42
Line 3 - -= 运算符实例,c 的值 = 21
Line 4 - *= 运算符实例,c 的值 = 441
Line 5 - /= 运算符实例,c 的值 = 21
Line 6 - %= 运算符实例,c 的值 = 11
Line 7 - <<= 运算符实例,c 的值 = 44
Line 8 - >>= 运算符实例,c 的值 = 11
Line 9 - &= 运算符实例,c 的值 = 2
Line 10 - ^= 运算符实例,c 的值 = 0
Line 11 - |= 运算符实例,c 的值 = 2
杂项运算符 ↦ sizeof & 三元
下表列出了 C 语言支持的其他一些重要的运算符,包括 sizeof 和 ? :。
运算符 | 描述 | 实例 |
---|---|---|
sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4,其中 a 是整数。 |
& | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
* | 指向一个变量。 | *a; 将指向一个变量。 |
? : | 条件表达式 | 如果条件为真 ? 则值为 X : 否则值为 Y |
实例
#include <stdio.h> int main() { int a = 4; short b; double c; int* ptr; /* sizeof 运算符实例 / printf("Line 1 - 变量 a 的大小 = %lu\n", sizeof(a) ); printf("Line 2 - 变量 b 的大小 = %lu\n", sizeof(b) ); printf("Line 3 - 变量 c 的大小 = %lu\n", sizeof(c) ); / & 和 * 运算符实例 / ptr = &a; / 'ptr' 现在包含 'a' 的地址 / printf("a 的值是 %d\n", a); printf("ptr 是 %d\n", ptr); / 三元运算符实例 */ a = 10; b = (a == 1) ? 20: 30; printf( "b 的值是 %d\n", b ); b = (a == 10) ? 20: 30; printf( "b 的值是 %d\n", b ); }
当上面的代码被编译和执行时,它会产生下列结果:
Line 1 - 变量 a 的大小 = 4
Line 2 - 变量 b 的大小 = 2
Line 3 - 变量 c 的大小 = 8
a 的值是 4
*ptr 是 4
b 的值是 30
b 的值是 20
C 中的运算符优先级
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。
例如 x = 7 + 3 * 2,在这里,x 被赋值为 13,而不是 20,因为运算符 * 具有比 + 更高的优先级,所以首先计算乘法 3*2,然后再加上 7。
下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
类别 | 运算符 | 结合性 |
---|---|---|
后缀 | () [] -> . ++ - - | 从左到右 |
一元 | + - ! ~ ++ - - (type)* & sizeof | 从右到左 |
乘除 | * / % | 从左到右 |
加减 | + -s | 从左到右 |
移位 | << >> | 从左到右 |
关系 | < <= > >= | 从左到右 |
相等 | == != | 从左到右 |
位与 AND | & | 从左到右 |
位异或 XOR | ^ | 从左到右 |
位或 OR | | | 从左到右 |
逻辑与 AND | && | 从左到右 |
逻辑或 OR | || | 从左到右 |
条件 | ?: | 从右到左 |
赋值 | = += -= *= /= %=>>= <<= &= ^=|= | 从右到左 |
逗号 | , | 从左到右 |
位运算符的优先级(从高到低):、&、^、|【其中(取反)的结合方向自右至左,且优先级高于算术运算符,其余运算符的结合方向都是自左至右,且优先级低于关系运算符】
实例
#include <stdio.h> int main() { int a = 20; int b = 10; int c = 15; int d = 5; int e; e = (a + b) * c / d; // ( 30 * 15 ) / 5 printf("(a + b) * c / d 的值是 %d\n", e ); e = ((a + b) * c) / d; // (30 * 15 ) / 5 printf("((a + b) * c) / d 的值是 %d\n" , e ); e = (a + b) * (c / d); // (30) * (15/5) printf("(a + b) * (c / d) 的值是 %d\n", e ); e = a + (b * c) / d; // 20 + (150/5) printf("a + (b * c) / d 的值是 %d\n" , e ); return 0; }
当上面的代码被编译和执行时,它会产生下列结果:
(a + b) * c / d 的值是 90
((a + b) * c) / d 的值是 90
(a + b) * (c / d) 的值是 90
a + (b * c) / d 的值是 50
常用库函数
那些已经编译好的,通过调用头文件就可以运用的函数叫库函数.
<math.h>(常用)(后续学完回来学习其具体原理):
- fabs(x) //求x的绝对值
- sqrt(x) //求x的开方
- sin和cos
- pow(x,n) //求x的n次方
- exp(x) //e的m次方
<ctype.h>(常用):
- isdigit(x) //判断x是否为数字
- isalpha(x) //判断x是否为字母
- isupper(x) //判断x是否为大写
- tolower(x) //转化为小写
- toupper(x) //转化为大写
其他:
<stdlib.h>:
-
srand():初始化发生器,一般用time(0)即系统时间初始化,time(0)需要<time.h>.
srand()函数原型:void srand (usigned int seed);
-
rand():随机数发生器,其实是伪随机,采取线性同余法 要大学知识,学完回来修改
rand()函数原型:int rand(void);具体使用:比如产生10~30的随机整数:srand(time(0)或time(null)); int a = rand() % (21)+10;因此,如要产生[m,n]范围内的随机数num,可用:
int num=rand()%(n-m+1)+m(m如果没写默认为0);
-
动态内存也是它.
程序流程控制
基本结构:顺序结构,分支结构,循环结构
分支结构(C语言中没有布尔,所以只能用0和非0表示真假,要注意)
if ()
{} //若只有一条代码可去掉花括号,else也可以,但要注意不要错误
else
{} //true即非0则执行if,反之执行else
if ()
{} //若为false跳过
多分支
if套if,会比较复杂
更建议switch
switch(x)
{
case 0:
xxxxxxxxx;
break; //必须有break,否则会继续执行下一个case的条件知道全部执行或者遇到break
case 1:
xxxxxx;
break;
default:
break; //default是没有满足case情况的条件才会执行,如果去掉且没有满足case的条件,跳出switch
}
循环结构
while () //单纯根据条件循环,每次循环好会检查一遍
do-while
do
{
xxxxxxx;
}while (xxxx); //先执行循环体再检查条件,注意条件后要加";"
for
for (x;y;z) //x是先执行,但不参与循环 y是条件 z是一次循环后会执行一边
//三个都可为空,但不能扔掉";" 第二个为空意为条件为真
break和continue的区别,continue是指终止也仅仅只是本次循环,后检查循环条件
数组
数组初始化
C语言常见问题——数组初始化的四种方法_c语言数组初始化-CSDN博客
一维数组
定义:int a[5]这类的,int为类型,a为首字母地址,5为长度
存储在连续内存中,根据个数和类型占空间,内存字节数=元素个数*sizeof(类型),像上面的就占20个字节(大概,我忘了int占多少)
注意:引用范围为[0,长度-1]
初始化:如果没有初始化,只是定义,那系统会赋予随机数.
int a[4]={1,2,3,4}
int a[4]={1,2,3}(没给初始值的默认为0,如果为字符型,默认为'\0')
int a[]={1,2,3,4}(长度为初始化的长度)
查找和排序
- 顺序查找:一个一个查
- 二分查找:字面意思
- 冒泡排序:一个一个移动,先移动最大或最小的,再第二大或第二小,以此类推
- 选择排序:先设一个为最小值,依次比较,比到更小的就赋值,用更小的继续比,以此类推
二维数组
顾名思义,两个维度.形式:
int score[3][4](先行后列)
其实仍是一维排序,在内存里就是
[0][0],[0][1],[0][2],[0][3],[1][0],[1][1],[1][2],[1][3](如此依序排列)
初始化:
int a[2][3]={1,2,3,4,5,6}
int a[2][3]={{1,2,3},{4,5,6}}
int a[2][3]={1,2,3,4}(未初始化的为0即{1,2,3,4,0,0})
int a[2][3]={{1,2},{3,4}}(为{1,2,0,3,4,0})
可省略行数,但不可省略列数,以上所有把行数去掉依旧等价.
函数
函数定义和调用
int(返回值类型) fact(函数名)(int n(形参类型))
{
xxxxxx;
return n;(返回值)
}
注意: 函数中定义的变量不能在另一个函数中使用,二者唯一的关系就是实参传递为形参,指针的替换. 有一种类似的即for()之类的,在里面定义的函数与外界不同,即使名字一样,原因:"在for循环的括号内声明变量,编译器会分配一块新的内存,这是一个全新的局部变量,而且只在循环内有效。for循环在这里相当于一个大括号(语句块)内一个局部变量。跳出循环,这个变量就没用了。但是,你在for循环声明的那个和sum和i 是在另一块内存里面,他们不在一个房间,即使他们的名字都一样,但是互不影响。对于int sum,int i;这种声明变量的方式是不允许的,不符合语法规范的。无论是在循环内还是循环外。"所以应该在函数外声明.
若返回值类型设为"void",表示函数没有返回值,可不加return,也可加return
C语言的函数可以递归,嵌套调用,但是(函数)不能嵌套定义。
c语言若不给函数写类型则为int型,故int main 可写成main
指针
指针即数据在内存的地址,变量地址即变量第一个字节所占的地址.一个好玩的:指针能指向指针,只要用"**".
数组与指针
a[5]中a即是a[0]的地址,a+1是a[1]的地址,以此类推.
而二维数组,比如a[2] [3],a是a[0] [0]的地址,但a+1是a[1] [0]的地址,以此类推.a[i]+j指向a[i] [j],也可替换成*(a+i)+j
注意:写代码时a+i<a+j(a为指针,i<j),但带星号就不一定了,但必须是指针相比
指针的nb
在局部函数中可以通过替换指针所指来改变变量
指向函数的指针
关于函数的介绍请参见 C++ 函数 章节。
简单地说,要调用一个函数,需要知晓该函数的参数类型、个数以及返回值类型,这些也统一称作接口类型。
可以通过函数指针调用函数。有时候,若干个函数的接口类型是相同的,使用函数指针可以根据程序的运行 动态地 选择需要调用的函数。换句话说,可以在不修改一个函数的情况下,仅通过修改向其传入的参数(函数指针),使得该函数的行为发生变化。
假设我们有若干针对 int
类型的二元运算函数,则函数的参数为 2 个 int
,返回值亦为 int
。下边是一个使用了函数指针的例子:
#include <iostream>
int (*binary_int_op)(int, int);
int foo1(int a, int b) { return a * b + b; }
int foo2(int a, int b) { return (a + b) * b; }
int main() {
int choice;
std::cin >> choice;
if (choice == 1) {
binary_int_op = foo1;
} else {
binary_int_op = foo2;
}
int m, n;
std::cin >> m >> n;
std::cout << binary_int_op(m, n);
}
在 C 语言中,诸如
void (*p)() = foo;
、void (*p)() = &foo;
、void (*p)() = *foo;
、void (*p)() = ***foo
等写法的结果是一样的。因为函数(如
foo
)是能够被隐式转换为指向函数的指针的,因此void (*p)() = foo;
的写法能够成立。使用
&
运算符可以取得到对象的地址,这对函数也是成立的,因此void (*p)() = &foo;
的写法仍然成立。对函数指针使用
*
运算符可以取得指针指向的函数,而对于**foo
这样的写法来说,*foo
得到的是foo
这个函数,紧接着又被隐式转换为指向foo
的指针。如此类推,**foo
得到的最终还是指向foo
的函数指针;用户尽可以使用任意多的*
,结果也是一样的。同理,在调用时使用类似
(*p)()
和p()
的语句是一样的,可以省去*
运算符。
可以使用 typedef
关键字声明函数指针的类型。
typedef int (*p_bi_int_op)(int, int);
这样我们就可以在之后使用 p_bi_int_op
这种类型,即指向「参数为 2 个 int
,返回值亦为 int
」的函数的指针。
可以通过使用 std::function
来更方便的引用函数。(未完待续)
使用函数指针,可以实现「回调函数」。(未完待续)
字符串
字符数组与字符串
字符串本质上是字符数组加上'\0'
学到了奇怪东西
单双引号
我发现单双引号的区别:单引号在字符常量时使用,表示单个字符。
例如:
char c; c = 'a'; c = '1'; c = 'A';
当在单引号中出现两个及以上字符时或没有字符时,编译出错。
例如:
char c = 'aA'; // 编译出错,单引号只能是一个字符
char c = ''; // 单引号中间没有任何字符时,编译出错
双引号在表示字符串常量时使用,可以表示0到多个字符组成的字符串。
char s1[] = "a";
char s2[] = "a1A";
char s3[] = ""; // 双引号中间可以没有任何字符,表示空字符串
单引号和双引号如何在程序中表示和输出自身呢?
和其它特殊字符一样,使用转义方式。
char c1 = ''' ; // 单引号字符
char c2 = '"'; // 双引号字符
同理,字符串中输出引号也是一样,直接使用转义方式表示。
总结:
1.字符常量使用单引号,字符串常量使用双引号表示
2.两者均支持转义字符表示,转义字符形式可以参见之前文章。
指针和字符串
char sa[10];gets(sa);合法 char*sp;gets(sp);不合法 因为后一个只是存储一个指针的数据,并没有初始化,所以这样做会造成一些数据没掉
正确的 char sa[10]="hello"; char*sp="hello";
字符串常见处理函数(string.h)
头文件为:<string.h>
字符串长度函数:int strlen(char*s)
举例:
char c[10]="Hello";
strlen(c)的值为5
strlen(c+2)为3
字符串拷贝函数:char* strcpy(char *s1,char *s2) //第一个是要拷贝到的位置,第二个是要拷贝的字符串
举例:
char c[10]="012345678";
char* s1=c;
char* s2="CHN";
strcpy(s1,s2);
则c变成{C,H,N,'\0',4,5,6,7,8,'\0'}
字符串连接函数:char* strcat(char* s1,char* s2) //将s2放在s1后面, 返回值是首字母的地址
举例:
char c[16]="HangZhou ";
char a[]="China";
stract(c,a);
则c变成{H,a,n,g,Z,h,o,u, ,C,h,i,n,a,'\0', }
字符串比较函数:int strcmp(char *s1,char *s2) //两个字符串从首字母开始比较,如果都一样,则返回0,如果不一样比较首次出现不同字母,比较两个字母大小,前一个大返回1,反之为-1.
sprintf
-
该函数包含在stdio.h的头文件中。
-
sprintf和平时我们常用的printf函数的功能很相似。sprintf函数打印到字符串中(要注意字符串的长度要足够容纳打印的内容,否则会出现内存溢出),而printf函数打印输出到屏幕上。sprintf函数在我们完成其他数据类型转换成字符串类型的操作中应用广泛。
-
sprintf函数的格式:
int sprintf( char *buffer, const char format [, argument,…] );
除了前两个参数固定外,可选参数可以是任意个。buffer是字符数组名;format是格式化字符串(像:”%3d%6.2f%#x%o”,%与#合用时,自动在十六进制数前面加上0x)。只要在printf中可以使用的格式化字符串,在sprintf都可以使用。其中的格式化字符串是此函数的精华。
printf 和sprintf都使用格式化字符串来指定串的格式,在格式串内部使用一些以”%”开头的格式说明符来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要的字符串。 -
可以控制精度
char str[20];
double f=14.309948;
sprintf(str,”%6.2f”,f); -
可以将多个数值数据连接起来
char str[20];
int a=20984,b=48090;
sprintf(str,”%3d%6d”,a,b);
str[]=”20984 48090” -
可以将多个字符串连接成字符串
char str[20];
char s1[5]={‘A’,’B’,’C’};
char s2[5]={‘T’,’Y’,’x’};
sprintf(str,”%.3s%.3s”,s1,s2);
%m.n在字符串的输出中,m表示宽度,字符串共占的列数;n表示实际的字符数。%m.n在浮点数中,m也表示宽度;n表示小数的位数。 -
可以动态指定,需要截取的字符数
char str[20];
char s1[5]={‘A’,’B’,’C’};
char s2[5]={‘T’,’Y’,’x’};
sprintf(str,”%.s%.s”,2,s1,3,s2);
sprintf(str, “%.f”, 10, 2, 3.1415926); -
可以打印出i的地址
char str[20];
int i;
sprintf(str, “%p”, &i);
上面的语句相当于
sprintf(str, “%0x”, 2 * sizeof(void *), &i); -
sprintf的返回值是字符数组中字符的个数,即字符串的长度,不用在调用strlen(str)求字符串的长度。
-
使用字符指针指向的字符串来接收打印的内容
例子:int main() { int ddd=666; char *buffer=NULL; if((buffer = (char *)malloc(80*sizeof(char)))==NULL) { printf("malloc error\n"); } sprintf(buffer, "The value of ddd = %d", ddd);//The value of ddd = 666 printf("%s\n",buffer); free(buffer); buffer=NULL; return 0; }
指针刚开始定义的时候,并不指向所处,可以指向一个变量,然后可以用,如果要单纯用这个指针,那么要给这个指针malloc分配一片内存,加了malloc就要加stdlib.h.
11.设想当你从数据库中取出一条记录,然后希望把他们的各个字段按照某种规则连接成一个字符串时,就可以使用这种方法,从理论上讲,他应该比strcat 效率高,因为strcat 每次调用都需要先找到最后的那个字符串结束字符’\0的位置,而在上面给出的例子中,我们每次都利用sprintf 返回值把这个位置直接记下来了。
例子:
void main(void)
{
char buffer[200], s[] = "computer", c = 'l';
int i = 35, j;
float fp = 1.7320534f; //
j = sprintf( buffer, " String: %s\n", s ); //
j += sprintf( buffer + j, " Character: %c\n", c ); //
j += sprintf( buffer + j, " Integer: %d\n", i ); //
j += sprintf( buffer + j, " Real: %f\n", fp );//
printf( "Output:\n%s\ncharacter count = %d\n", buffer, j );
}
该例子是将所有定义的数据和格式控制块中的字符连接在一起,最后打印出来buffer的内容和字符串中字符的个数。
结果如图所示:
12、 格式化数字字符串
sprintf最常见的应用之一莫过于把整数打印到字符串中。如:
(1)把整数123打印成一个字符串保存在s中。
sprintf(s, “%d”, 123); //产生“123″
(2)可以指定宽度,不足的左边补空格:
sprintf(s, “%8d%8d”, 123, 4567); //产生:“ 123 4567″
当然也可以左对齐:
sprintf(s, “%-8d%8d”, 123, 4567); //产生:“123 4567″
(3)也可以按照16进制打印:
sprintf(s, “%8x”, 4567); //小写16进制,宽度占8个位置,右对齐
sprintf(s, “%-8X”, 4568); //大写16进制,宽度占8个位置,左对齐
这样,一个整数的16进制字符串就很容易得到,但我们在打印16进制内容时,通常想要一种左边补0的等宽格式,那该怎么做呢?很简单,在表示宽度的数字前面加个0就可以了。
sprintf(s, “%08X”, 4567); //产生:“000011D7″
上面以”%d”进行的10进制打印同样也可以使用这种左边补0的方式。
这里要注意一个符号扩展的问题:比如,假如我们想打印短整数
(4)(short)-1的内存16进制表示形式,在Win32平台上,一个 short型占2个字节,所以我们自然希望用4个16进制数字来打印它:
short si = -1;
sprintf(s, “%04X”, si);
产生“FFFFFFFF,怎么回事?因为 sprintf是个变参函数,除了前面两个参数之外,后面的参数都不是类型安全的,函数更没有办法仅仅通过一个“%X”就能得知当初函数调用前参数压栈时 被压进来的到底是个4字节的整数还是个2字节的短整数,所以采取了统一4字节的处理方式,导致参数压栈时做了符号扩展,扩展成了32位的整数-1,打印时 4个位置不够了,就把32位整数-1的8位16进制都打印出来了。如果你想看si的本来面目,那么就应该让编译器做0扩展而不是符号扩展(扩展时二进制左边补0而不是补符号位):
sprintf(s, “%04X”, (unsigned short)si);
就可以了。或者:
unsigned short si = -1;
sprintf(s, “%04X”, si);
sprintf和printf还可以按8进制打印整数字符串,使用”%o”。注意8进制和16进制都不会打印出负数,都是无符号的,实际上也就是变量的内部编码的直接用16进制或8进制表示。
指针数组
举例:char *pc[6]={"red","green","yellow",xxxx} //存的是地址
结构体
struct S {
char a[20];
char b[5];
float c;
};
如此定义
可以在花括号后直接定义 也可以struct S a;
子项目要读取可以这样打S.a或者S->a,注意类型,特别是指针
令结构体等于{0}等同于初始化.
存储,链接,内存管理
作用域和链接
变量的作用范围就是作用域,在函数里就是代码块作用域,之外即所有函数可见,为全局变量,作用域为文件作用域,包括函数,是从声明位置到末尾.
变量的就近原则
这就引出了一个问题,如果在文件头定义了一个全局变量.那么后面忘了又定义一个同名局部变量就毁了.所以编译器会根据就近原则改变.所以这里建议是根据就近原则写变量,全局变量要小心点使用.
链接
链接就是把编译过的源代码和头代码与库代码这些连在一起.
链接属性:变量带有标识符,一种是外部一种是内部一种是没有,即代表多个文件中同名变量相同,单个中相同,不相同.所以为了保证变量一样,往往用external声明.
static可以把外部改为内部,但有两个条件:1.必须是文件作用域,其次是改了就改不回去了.
生存期与存储类型
我们说研究变量的作用域和链接属性是从空间的角度进行分析的,那么研究变量的生存期又是从什么角度进行分析的呢?
答:时间的角度,说白了就是研究这个变量可以“活”多久。
请问具有静态生存期的变量可以存活多久? 答:活到程序关闭为止。
局部变量和函数的形式参数属于什么类型的生存期?答:自动存储期。具有代码块作用域的变量一般情况下具有自动存储期(比如局部变量和形式参数),具有自动存储期的变量在代码块结束时将自动释放存储空间。
C 语言提供的 5 种存储类型(auto,register,staticx(data段),extern,typedev)中,理论上哪一种的执行效率是最高的?答:register,因为寄存器是存在于 CPU 的内部的,CPU 对寄存器的读取和存储可以说是几乎没有任何延迟。
被定义为 AUTO 存储类型的变量,具有什么样的作用域、生存期和链接属性呢?
答:自动变量拥有代码块作用域,自动存储期和空链接属性。
当一个程序被分割为多个源代码文件进行编译时,为什么全局变量可以从其他源代码文件中进行引用?答:因为具有文件作用域的变量(比如全局变量)默认具有 External 链接属性,而 External 链接属性“在多个文件中声明的同名标识符表示同一个实体”。
问:各自特性?
答:一般设立的局部变量就是auto,写出auto是为了更清楚.register把变量放在寄存器,注意,一般这时他的地址不可获取.其他的不介绍.
动态内存管理
动态内存管理(4种函数的详解)
C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了
一.malloc
C语言提供了⼀个动态内存开辟的函数:
void* malloc (size_t size);
这个函数向内存申请⼀块连续可用的空间,并返回指向这块空间的指针。
• 如果开辟成功,则返回⼀个指向开辟好空间的指针。
• 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
• 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
• 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
ps:它不会初始化,所以请初始化,有个string.h库里的函数
void *memset(void *str, int c, size_t n):用一个常量字节填充内存空间,可用于初始化
void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。
memmove:复制内存空间
memcmp:比较内存空间
memchr:内存中搜一个字符
二.free
C语言提供了另外⼀个函数free,是用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
• 如果参数 ptr 是NULL指针,则函数什么事都不做。
• malloc和free都声明在 stdlib.h 头⽂件中。
例如:
include <stdio.h>
include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int* )malloc( num * sizeof(int) );
if(NULL != ptr) //判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr); //释放ptr所指向的动态内存
ptr = NULL; //防止其变为野指针
return 0;
}
三.calloc
C语⾔还提供了⼀个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
• 函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
例子:
include <stdio.h>
include <stdlib.h>
int main()
{
int i, n;
int *a;
printf("要输入的元素个数:");
scanf("%d",&n);
a = (int*)calloc(n, sizeof(int));
printf("输入 %d 个数字:\n",n);
for( i=0 ; i < n ; i++ )
{
scanf("%d",&a[i]);
}
printf("输入的数字为:");
for( i=0 ; i < n ; i++ ) {
printf("%d ",a[i]);
}
free (a); // 释放内存
return(0);
}
所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便地使用calloc函数来完成任务。
四.realloc
• realloc函数的出现让动态内存管理更加灵活。
• 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们⼀定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
原型如下:
void* realloc (void* ptr, size_t size);
• ptr 是要调整的内存地址
• size 调整之后新大小
• 返回值为调整之后的内存起始位置。
• 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc的运用:
define _CRT_SECURE_NO_WARNINGS
include<stdio.h>
include<stdlib.h>
void readNumbers(int* arr, int size) {
printf("输入%d个数字的值\n", size);
for (int i = 0; i < size; i++) {
scanf("%d", &arr[i]);
}
}
void printNumbers(const int* arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main() {
printf("请输入要创建数字的个数n\n");
int n1 = 0;
scanf("%d", &n1);
int* p = (int*)calloc(n1, sizeof(int));//创建动态内存,将其初始化为0
if (!p) {
perror("内存分配失败");
return EXIT_FAILURE;
}
readNumbers(p, n1);
printNumbers(p, n1);
printf("请输入要新添加数字的个数n\n");
int n2 = 0;
scanf("%d", &n2); int* p2 = (int*)realloc(p, (n1 + n2) * sizeof(int));//将动态内存添加为(n1+n2)
if (!p2) {
free(p);
perror("内存重新分配失败");
return EXIT_FAILURE;
}
readNumbers(p2, n1 + n2);
printNumbers(p2, n1 + n2);
free(p2);
return 0;
}
读入整数N,再读入N个整数,将这N个整数从小到大排序后输出。(不能定义整型数组,用动态内存技术实现)
输入样例:
5
1 5 3 4 2
输出样例:
1 2 3 4 5
int main()
{int i;
int N;
scanf("%d",&N);
int *p;
p=(int )malloc(sizeof(int)N);
for(int i=0;i<N;i++)
{
scanf("%d",&p[i]);
}
for(i=1;i<N;i++){
for(int j=0;j<N-i;j++){
if(p[j]>p[j+1])
{
int temp=p[j];
p[j]=p[j+1];
p[j+1]=temp;
}
}
}
for(i=0;i<N-1;i++)
{
printf("%d ",p[i]);
}
printf("%d",p[N-1]);
return 0;
}
内存布局
<C语言的内存布局(详细分析变量在.bss和DS段的分布)_bss dss-CSDN博客>
Woshiluo’s Doubt
这真是美好的一天,鸟儿在歌唱,花朵绽放。在像这样美丽的日子里,你这样的孩子......应该来给Woshiluo 答疑解惑!某天,Woshiluo 拿到了下面这串代码。
#include<stdio.h>
#include<stdint.h>
#include<inttypes.h>
int64_t a[100];
int main(){
int64_t b[100]={0};
int64_t c[100];
printf("[10]: %" PRId64 " size: %zu\n", a[10],sizeof(a));
printf("[10]: %" PRId64 " size: %zu\n", b[10],sizeof(b));
printf("[10]: %" PRId64 " size: %zu\n", c[10],sizeof(c));
int64_t*pa = a,*pb = b,*pc = c;
printf("[10]: %" PRId64 " size: %zu\n", pa[10],sizeof(pa));
printf("[10]: %" PRId64 " size: %zu\n", pb[10],sizeof(pb));
printf("[10]: %" PRId64 " size: %zu\n", pc[10],sizeof(pc));}
Woshiluo 跑了一下,结果如下。[10]: 0 size: 800[10]: 0 size: 800[10]: 140737225471344 size: 800[10]: 0 size: 8[10]: 0 size: 8[10]: 140737225471344 size: 8
Woshiluo 感到非常疑惑!具体来说,他对以下内容感到疑惑:1.a和c具有相同的声明语句,为什么访问相同位置的结果并不相同。而b的结果又为何和a一致。2.papb和pc均能正常访问对应数组的元素,为什么sizeof运算符得到的结果不同。3.为什么使用PRId64而非lld,以及为什么使用%zu而不是%lu来输出对应变量。Woshiluo 很快又找到了新的好玩的,他决定将这个疑惑交给Kira Kira 的你来解决。如果认为你可以解答这些疑惑,请向Woshiluo 的qq 私信发送你的解答。鉴于Woshiluo 已经多日未眠,实在难以思考。他希望你能够尽可能详细的给出解答。
Woshiluo’ Result
本人水平有限,如有问题敬请指出。其实三个问题难度不是递增的,而且这个程序的输出不是确定性的,因为有UB(未定义行为)。Q1变量初始化的三两事。首先a和c都没有初始化。但是a是全局变量,c是局部变量,一般来说全局变量放在bss段中,bss段会以 0 初始化(不是自定义的初始化)。而c就在栈上了,栈上是没有初始化的,那就内存里是啥就读到啥了。b虽然初始化了,但是我们只显示定义了一个变量,这就是数组初始化的有趣之处了:「All array elements that are not initialized explicitly are empty-initialized.」source: https://en.cppreference.com/w/c/language/array_initialization
Q2sizeof是运算符哦!sizeof返回的是变量类型的大小。abc是int64_t [100],这个就是8∗100=800 bytes 了。但是papbpc是int64_t*,这个大小取决于机器字长。
Q3格式化症候群。翻阅文档:https://en.cppreference.com/w/c/io/fprintf我们可以看到%lld是long long int的格式化字符串。但是我们这里是int64_t,也就是Fixed width integer types。接着翻阅文档:https://en.cppreference.com/w/c/language/arithmetic_types我们可以看到,对于intlong long这种Arithmetic types,规范给的要求都是带at least的。也就是说%lld可能对应的并不是 64 位整数。那么我们要怎么输出定宽整型呢?还是翻阅文档:https://en.cppreference.com/w/c/types/integerinttypes.h文件提供了Format macro constants,我们直接用就是了,也就是这里的PRId64。接下来是%zu。翻阅sizeof相关文档:https://en.cppreference.com/w/c/language/sizeof「Both versions return a value of type size_t. 」也就是说我们现在要输出的是size_t,回到上面的文档,我们可以查到对应的格式化字符串为%zu。
内联函数
用inline解决,在编译过程中直接将函数替换,从而解决宏定义的缺点,提高了效率,但会减慢编译速度,而且现在的编译器能自己判断哪些需要内联,所以只是一个需要了解的知识点.
单链表
数据结构与算法——单链表的实现及原理 - 索智源 - 博客园 (cnblogs.com)
数组的特点
在内存中,数组是一块连续的区域。
数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费内存空间。
插入数据和删除数据效率低,插入数据时,这个位置后面的数据在内存中都要向后移。
随机读取效率很高。因为数组是连续的,知道每一个数据的内存地址,可以直接找到给地址的数据。
并且不利于扩展,数组定义的空间不够时要重新定义数组。
链表的特点
在内存中可以存在任何地方,不要求连续。
每一个数据都保存了下一个数据的内存地址,通过这个地址找到下一个数据。 第一个人知道第二个人的座位号,第二个人知道第三个人的座位号……
增加数据和删除数据很容易。 再来个人可以随便坐,比如来了个人要做到第三个位置,那他只需要把自己的位置告诉第二个人,然后问第二个人拿到原来第三个人的位置就行了。其他人都不用动。
查找数据时效率低,因为不具有随机访问性,所以访问某个位置的数据都要从第一个数据开始访问,然后根据第一个数据保存的下一个数据的地址找到第二个数据,以此类推。 要找到第三个人,必须从第一个人开始问起。
不指定大小,扩展方便。链表大小不用定义,数据随意增删。
各自的优缺点
数组的优点
随机访问性强
查找速度快
- 数组的缺点
插入和删除效率低
可能浪费内存
内存空间要求高,必须有足够的连续内存空间。
数组大小固定,不能动态拓展 - 链表的优点
插入删除速度快
内存利用率高,不会浪费内存
大小没有固定,拓展很灵活。 - 链表的缺点
不能随机查找,必须从第一个开始遍历,查找效率低
这里可以用单链表把free()释放的内存空间连成一起(存他们的地址),因为他们不是连续的,会造成内存的支离破碎,通过单链表就可以利用这些垃圾.
typedef
typedef 可以取绰号,包括系统库里内置的函数 .
共用体(union)
其实等于只有一个地址的结构体,以最后赋值的值为内容,最大占其中最大成员的内存.语法与结构体一致.
枚举类型(enum)
enum color{r,y,g,b}; //则r=0,y=1,以此类推;当然也可以直接设定r=10,那y等于11,以此类推.
r=1111 //注意,enum在编译时就设定为常量,不可修改,这样写会报错.
位域
位域
为了节省内存,C语言支持使用字节内部的比特,下例:
struct 1{
int a:1; //表示给a一个比特的位置
}
当然,给的位域肯定不能超过类型本身.还有不是全部的类型都支持.
位操作
~(按位取反:顾名思义) > &(按位与:同时为1才为1) > ^(按位异或:相等为0,不同为1) > |(按位或:一个为1即可)
文件
fopen,fclose
fopen 函数用于打开一个文件并返回文件指针。
函数原型:
#include <stdio.h>
...
FILE *fopen(const char *path, const char *mode);
参数解析:
参数 | 含义 |
---|---|
path | 该参数是一个 C 语言字符串,指定了待打开的文件路径和文件名(见备注) |
mode | 1. 该参数是一个 C 语言字符串,指定了文件的打开模式 2. 下面列举了所有可使用的打开模式:模式****描述"r"1. 以只读的模式打开一个文本文件,从文件头开始读取 2. 该文本文件必须存在 "w"1. 以只写的模式打开一个文本文件,从文件头开始写入 2. 如果文件不存在则创建一个新的文件 3. 如果文件已存在则将文件的长度截断为 0(重新写入的内容将覆盖原有的所有内容) "a"1. 以追加的模式打开一个文本文件,从文件末尾追加内容 2. 如果文件不存在则创建一个新的文件 "r+"1. 以读和写的模式打开一个文本文件,从文件头开始读取和写入 2. 该文件必须存在 3. 该模式不会将文件的长度截断为 0(只覆盖重新写入的内容,原有的内容保留) "w+"1. 以读和写的模式打开一个文本文件,从文件头开始读取和写入 2. 如果文件不存在则创建一个新的文件 3. 如果文件已存在则将文件的长度截断为 0(重新写入的内容将覆盖原有的所有内容) "a+"1. 以读和追加的模式打开一个文本文件 2. 如果文件不存在则创建一个新的文件 3. 读取是从文件头开始,而写入则是在文件末尾追加 "b"1. 与上面 6 中模式均可结合("rb", "wb", "ab", "r+b", "w+b", "a+b") 2. 其描述的含义一样,只不过操作的对象是二进制文件(见备注) |
返回值:
- 如果文件打开成功,则返回一个指向 FILE 结构的文件指针;
- 如果文件打开失败,则返回 NULL 并设置 errno 为指定的错误。
备注
- path 参数可以是相对路径(../fishc.txt)也可以是绝对路径(/home/FishC/fishc.txt),如果只给出文件名而不包含路径,则表示该文件在当前文件夹中
- 从本质上来说,文本文件也是属于二进制文件的,只不过它存放的是相应的字符编码值。
- 打开方式要区分文本模式和二进制模式的原因,主要是因为换行符的问题。C 语言用 \n 表示换行符,Unix 系统用 \n,Windows 系统用 \r\n,Mac 系统则用 \r。如果在 Windows 系统上以文本模式打开一个文件,从文件读到的 \r\n 将会自动转换成 \n,而写入文件则将 \n 替换为 \r\n。但如果以二进制模式打开则不会做这样的转换。Unix 系统的换行符跟 C 语言是一致的,所以不管以文本模式打开还是二进制模式打开,结果都是一样的。
演示:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ((fp = fopen("hello.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
while ((ch = getc(fp)) != EOF)
{
putchar(ch);
}
fclose(fp); //一定别忘了
return 0;
}
fputs fgets fputc
函数概要:
fgetc 函数用于从文件流中读取下一个字符并推进文件的位置指示器(用来指示接下来要读写的下一个字符的位置)。
函数原型:
#include <stdio.h>
...
int fgetc(FILE *stream);
参数解析:
参数 | 含义 |
---|---|
stream | 该参数是一个 FILE 对象的指针,指定一个待读取的文件流 |
返回值:
- 该函数将读取到的 unsigned char 类型转换为 int 类型并返回;
- 如果文件结束或者遇到错误则返回 EOF。
备注:
- fgetc 函数和 getc 函数两个的功能和描述基本上是一模一样的,它们的区别主要在于实现上:fgetc 是一个函数;而 getc 则是一个宏的实现
- 一般来说宏产生较大的代码,但是避免了函数调用的堆栈操作,所以速度会比较快。
- 由于 getc 是由宏实现的,对其参数可能有不止一次的调用,所以不能使用带有副作用(side effects)的参数。
小甲鱼温馨提示:
我知道上面第 3 点可能会让有些童鞋感到懵逼,我这里给大家简单解释下,所谓带有副作用的参数就是指 getc(fp++) 这类型的参数,因为参数在宏的实现中可能会被调用多次,所以你的想法是 fp++,而副作用下产生的结果可能是 fp++++++。
演示:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ((fp = fopen("hello.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_SUCCESS);
}
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch);
}
fclose(fp);
return 0;
}
函数概要:
fputc 函数用于将一个字符写入到指定的文件中并推进文件的位置指示器(用来指示接下来要读写的下一个字符的位置)。
函数原型:
#include <stdio.h>
...
int fputc(int c, FILE *stream);
参数解析:
参数 | 含义 |
---|---|
c | 指定待写入的字符 |
stream | 该参数是一个 FILE 对象的指针,指定一个待写入的文件流 |
返回值:
- 如果函数没有错误,返回值是写入的字符;
- 如果函数发生错误,返回值是 EOF。
备注:
- fputc 函数和 putc 函数两个的功能和描述基本上是一模一样的,它们的区别主要在于实现上:fputc 是一个函数;而 putc 则是一个宏的实现
- 一般来说宏产生较大的代码,但是避免了函数调用的堆栈操作,所以速度会比较快。
- 由于 putc 是由宏实现的,对其参数可能有不止一次的调用,所以不能使用带有副作用(side effects)的参数。
小甲鱼温馨提示:
我知道上面第 3 点可能会让有些童鞋感到懵逼,我这里给大家简单解释下,所谓带有副作用的参数就是指 putc('X', fp++) 这类型的参数,因为参数在宏的实现中可能会被调用多次,所以你的想法是 fp++,而副作用下产生的结果可能是 fp++++++。
演示:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ((fp = fopen("file.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
for (ch = 33; ch <= 100; ch++)
{
fputc(ch, fp);
}
fputc('\n', fp);
fclose(fp);
return 0;
}
函数概要:
fputs 函数用于将一个字符串写入到指定的文件中,表示字符串结尾的 '\0' 不会被一并写入。
函数原型:
#include <stdio.h>
...
int fputs(const char *s, FILE *stream);
参数解析:
参数 | 含义 |
---|---|
s | 字符型指针,指向用于存放待写入字符串的位置 |
stream | 该参数是一个 FILE 对象的指针,指定一个待操作的数据流 |
返回值:
- 如果函数调用成功,返回一个非 0 值;
- 如果函数调用失败,返回 EOF。
演示:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ((fp = fopen("file.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fputs("I love FishC.com!\n", fp);
fclose(fp);
return 0;
}
feof
函数概要:
feof 函数用于检测文件的末尾指示器(end-of-file indicator)是否被设置。
函数原型:
#include <stdio.h>
...
int feof(FILE *stream);
参数解析:
参数 | 含义 |
---|---|
stream | 该参数是一个 FILE 对象的指针,指定一个待检测的文件流 |
返回值:
- 如果检测到末尾指示器(end-of-file indicator)被设置,返回一个非 0 值;
- 如果检测不到末尾指示器(end-of-file indicator)被设置,返回值为 0。
备注:
- feof 函数仅检测末尾指示器的值,它们并不会修改文件的位置指示器。
- 文件末尾指示器只能使用 clearerr 函数清除。
演示:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
int ch;
if ((fp = fopen("file.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
while (1)
{
ch = fgetc(fp);
if (feof(fp))
{
break;
}
putchar(ch);
}
fclose(fp);
return 0;
}
考考你,为啥小甲鱼这里不直接将 while 循环写成下面这样:
……
while (!feof(fp))
{
putchar(getc(fp));
}
……
因为fgetc每次只读取到1个字符,如果写成while (!feof(fp)) ,会因为fgetc少读取一个EOF,而造成了多读一次。
一些奇奇怪怪
判断整数
要判断一个数是否是整数,可以使用C语言中的取模运算符“%”,对该数进行取模操作,如果余数为0,则说明该数是整数,否则不是整数。示例代码如下:
if (num % 1 == 0) { printf("这个数是整数\n"); } else { printf("这个数不是整数\n"); }
在这个示例中,我们首先从用户输入获取一个数,然后使用 (int)
将其转换为整数类型。然后,我们将原始数减去转换后的整数,如果结果为0,则说明原始数是一个整数,否则不是。最后根据判断结果输出相应的信息。
烫烫烫
C语言中-858993460的由来。
在C语言里,我们定义一个变量 int i,不初始化,然后输出,会是什么结果呢?
在上面的代码实例中,我们很明显可以看出,C编译器,定义变量的栈空间填充的值默认是CC,因为i是一个int类型,那么即就是占四个字节。所以 ,未初始化的i填充的字节数就是 0xCCCCCCCC,那么输出
-858993460又是什么鬼?,其实我们不妨把 0xCCCCCCCC转换为二进制看看。
0xCCCCCCCC的二进制:
11001100110011001100110011001100
了解过负数在计算机是怎么存储的同学们都知道,二进制首位 是1 ,那么就代表这是个负数,所以我们不妨求其反码:(符号位不变,其他位取反)
原码:11001100110011001100110011001100
反码:10110011001100110011001100110011
再求其补码:(反码的基础上+1)10110011001100110011001100110100
那么这个数,再计算机内存的二进制即就是上面的补码,我们可以转换为十进制,答案就是 -858993460
还有解释一下,为什么,我们新手玩家,写程序写不好的时候会碰见好多烫烫烫,以为乱码了?其实不是的,跟乱码没关系,乱码是解码跟编码不同才会乱码,因为 中文 烫的 16进制刚好就是 0xCCCC;可以检查下程序哪里数组越界或者字符串是不是‘\0’结尾的。
另外还要说明一点的是,编译器再我们使用 堆空间,默认填充的是 CD。
我们可以看到,我申请了10个大小的堆空间内存,结果这段内存地址就 填充了 10个CD。
判断字符是否为数字或者字符既可以是比较ascii码,也可以是用相关函数.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了