C语言学习笔记(基础语法篇)

C语言学习笔记(基础语法篇)

序言

首先事先说明一下,这是我从各处整理的,当初刚接触CS,甚至连标注意识都没有,再次感谢写这些文章的人.当然这里不是说全部都是别人写的了,也有一点我自己的思考.

首先是几个注意点:

  1. 结构化,模块化,分而治之

  2. 多写注释,多调试

  3. 指针也有不同类型

  4. 在 C 语言中,所有的变量命名都必须遵循某些语法规则,才能算是有效的。一些主要要点:

    • 变量名可以包含字母、数字和下划线。
    • 变量名的首字母必须是字母或下划线。
    • C 语言是大小写敏感的–myVarmyvar是不同的变量。
    • 变量名不能包含空格或特殊字符,如 &*$等。
    • 名字不能与 C 语言关键字冲突,如 ifelseint等。
  5. 隐形类型转化要注意,是根据要求的数的类型.

  6. '' 引起的一个字符代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。

    "" 引起的字符串代表的是一个指向无名数组起始字符的指针。

  7. 搞指针建议初始化为null指针,减少错误.

  8. &&前面如果是0后面既不会判断,||前面如果是1后面就不会判断,所以无论后面有什么运算都不生效

  9. ++在右边先不参与计算,甚至是赋值,是最低等的

  10. 不要写10<=a<=100这种sb东西

  11. 数组必须用常量初始

  12. 结构体如果有指针要加括号

  13. ++是自增,也就是只有最开始的a会增加:b+a++,只有a会加1.

  14. ==

  15. 初始化

  16. 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

forint 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 相同):

img

为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用 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.

输入方式

  1. 一种就是for(i=0;(c[i]=getchar())!=xxxx;xxx);但是这个要在末尾加\0(但一般都有按回车所以可以直接把'\n'改为'\0')

  2. 另一种就是scanf("%s",c) //输入时不用&而是直接写数组名,自动加'\0',输入长度<数组长度(要留一位放'\0'),遇到空格,tab,回车结束.scanf()返回值为输入数据的个数,有一个就返回一个,两个就两个,如果没有就返回eof.可利用这个与while()组合使用.

  3. 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

  1. 该函数包含在stdio.h的头文件中。

  2. sprintf和平时我们常用的printf函数的功能很相似。sprintf函数打印到字符串中(要注意字符串的长度要足够容纳打印的内容,否则会出现内存溢出),而printf函数打印输出到屏幕上。sprintf函数在我们完成其他数据类型转换成字符串类型的操作中应用广泛。

  3. sprintf函数的格式:
    int sprintf( char *buffer, const char format [, argument,…] );
    除了前两个参数固定外,可选参数可以是任意个。buffer是字符数组名;format是格式化字符串(像:”%3d%6.2f%#x%o”,%与#合用时,自动在十六进制数前面加上0x)。只要在printf中可以使用的格式化字符串,在sprintf都可以使用。其中的格式化字符串是此函数的精华。
    printf 和sprintf都使用格式化字符串来指定串的格式,在格式串内部使用一些以”%”开头的格式说明符来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要的字符串。

  4. 可以控制精度
    char str[20];
    double f=14.309948;
    sprintf(str,”%6.2f”,f);

  5. 可以将多个数值数据连接起来
    char str[20];
    int a=20984,b=48090;
    sprintf(str,”%3d%6d”,a,b);
    str[]=”20984 48090”

  6. 可以将多个字符串连接成字符串
    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表示小数的位数。

  7. 可以动态指定,需要截取的字符数
    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);

  8. 可以打印出i的地址
    char str[20];
    int i;
    sprintf(str, “%p”, &i);
    上面的语句相当于
    sprintf(str, “%0x”, 2 * sizeof(void *), &i);

  9. sprintf的返回值是字符数组中字符的个数,即字符串的长度,不用在调用strlen(str)求字符串的长度。

  10. 使用字符指针指向的字符串来接收打印的内容
    例子:

    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. 其描述的含义一样,只不过操作的对象是二进制文件(见备注)

返回值:

  1. 如果文件打开成功,则返回一个指向 FILE 结构的文件指针;
  2. 如果文件打开失败,则返回 NULL 并设置 errno 为指定的错误。

备注

  1. path 参数可以是相对路径(../fishc.txt)也可以是绝对路径(/home/FishC/fishc.txt),如果只给出文件名而不包含路径,则表示该文件在当前文件夹中
  2. 从本质上来说,文本文件也是属于二进制文件的,只不过它存放的是相应的字符编码值。
  3. 打开方式要区分文本模式和二进制模式的原因,主要是因为换行符的问题。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 对象的指针,指定一个待读取的文件流

返回值:

  1. 该函数将读取到的 unsigned char 类型转换为 int 类型并返回;
  2. 如果文件结束或者遇到错误则返回 EOF。

备注:

  1. fgetc 函数和 getc 函数两个的功能和描述基本上是一模一样的,它们的区别主要在于实现上:fgetc 是一个函数;而 getc 则是一个宏的实现
  2. 一般来说宏产生较大的代码,但是避免了函数调用的堆栈操作,所以速度会比较快。
  3. 由于 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 对象的指针,指定一个待写入的文件流

返回值:

  1. 如果函数没有错误,返回值是写入的字符;
  2. 如果函数发生错误,返回值是 EOF。

备注:

  1. fputc 函数和 putc 函数两个的功能和描述基本上是一模一样的,它们的区别主要在于实现上:fputc 是一个函数;而 putc 则是一个宏的实现
  2. 一般来说宏产生较大的代码,但是避免了函数调用的堆栈操作,所以速度会比较快。
  3. 由于 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 对象的指针,指定一个待操作的数据流

返回值:

  1. 如果函数调用成功,返回一个非 0 值;
  2. 如果函数调用失败,返回 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 对象的指针,指定一个待检测的文件流

返回值:

  1. 如果检测到末尾指示器(end-of-file indicator)被设置,返回一个非 0 值;
  2. 如果检测不到末尾指示器(end-of-file indicator)被设置,返回值为 0。

备注:

  1. feof 函数仅检测末尾指示器的值,它们并不会修改文件的位置指示器。
  2. 文件末尾指示器只能使用 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码,也可以是用相关函数.

posted @   T0fV404  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示