C语言学习笔记

计算机相关

系统组成
  1. 硬件系统
    1. 主机
      1. 中央处理器(CPU)
        1. 寄存器
        2. 运算器
        3. 控制器
      2. 内存储器
        1. 随机存储器(RAM)
        2. 只读存储器(ROM)
    2. 外部设备(外设)
      1. 输入设备:鼠标、键盘、摄像头等
      2. 输出设备:声卡、显卡等
      3. 外存储器:光盘、硬盘、U盘等
  2. 软件系统
    1. 系统软件
      1. 操作系统
      2. 语言处理系统
      3. 数据库管理系统
      4. 系统服务程序
    2. 应用软件
内存储器与外存储器
  • 内存储器

采用电信号存储数据,速度快,但是断点数据丢失

  • 外存储器

如光盘,采用磁信号存储数据,速度慢,但是可以用来做数据持久化

数据计算时的存储变化

如一个磁盘中的数据:
磁盘 > 磁盘缓存 > 内存 > CPU缓存 > 寄存器 > 运算

寄存器
几个概念
  1. 寄存器是CPU内部的最基本的存储单元
  2. CPU通过总线(数据、地址、控制)来和外部设备打交道
  3. 如果CPU的总线是8位,寄存器也是8位,则这就是个8位的CPU;倘若总线16位,寄存器32位,那么则是一个准32位CPU
  4. 在64位的CPU上运行64位的操作系统,则这是个64位的操作系统,若运行的是32位的操作系统,则这是个32位的操作系统
  5. 64位的软件不能运行在32位的CPU上
寄存器名称
8位CPU16位CPU32位CPU64位CPU
AAXEAXRAX
BBXEBXRBX
CCXECXRCX
DDXEDXRDX
对寄存器的操作

C语言对寄存器做了封装,只能做些简单的操作,如果想进行更深层次的操作,需要使用汇编语言,对此,可以在C语言中嵌套编写汇编语言

#include <stdio.h>

int main(void)
{
	int a, b, c;
	__asm
	{
		mov a, 3 //将3放在a对应的内存中
		mov b, 4 //将4放在b对应的内存中
		mov eax, a //将a放入寄存器中
		add eax, b //将b放入寄存器中,并和其中的a进行加法运算,运算结果(7)依旧在寄存器中
		mov c, eax //将寄存器中的运算结果放入c对应的内存中
	}
	printf("c: %d\n", c); //7
	system("pause");
	return 0;
}

Visual Studio相关

VS执行程序时窗口闪退问题的解决方案
  1. 通过system函数
#include <stdio.h>

int main(void)
{
	printf("hello world\n");
	system("pause");
	return 0;
}
  1. 通过修改VS配置
右键项目 > 属性 > 配置属性 > 链接器 > 系统 > 子系统:控制台
代码片段管理
  1. 新建一个,如main函数的
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2019/CodeSnippet">
	<CodeSnippet Format="1.0.0">
		<Header>
			<Title>#1</Title>
			<Shortcut>#1</Shortcut>
			<Description>c语言main函数</Description>
			<Author>Microsoft Corporation</Author>
			<SnippetTypes>
				<SnippetType>Expansion</SnippetType>
				<SnippetType>SurroundsWith</SnippetType>
			</SnippetTypes>
		</Header>
		<Snippet>
			<Declarations>
				<Literal>
					<ID>expression</ID>
					<ToolTip>要计算的表达式</ToolTip>
					<Default>true</Default>
				</Literal>
			</Declarations>
			<Code Language="cpp"><![CDATA[#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

int main(void)
{
	$selected$$end$
	system("pause");
	return EXIT_SUCCESS;
}
					]]>
			</Code>
		</Snippet>
	</CodeSnippet>
</CodeSnippets>
<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2019/CodeSnippet">
	<CodeSnippet Format="1.0.0">
		<Header>
			<Title>#2</Title>
			<Shortcut>#2</Shortcut>
			<Description>c++语言main函数</Description>
			<Author>Microsoft Corporation</Author>
			<SnippetTypes>
				<SnippetType>Expansion</SnippetType>
				<SnippetType>SurroundsWith</SnippetType>
			</SnippetTypes>
		</Header>
		<Snippet>
			<Declarations>
				<Literal>
					<ID>expression</ID>
					<ToolTip>要计算的表达式</ToolTip>
					<Default>true</Default>
				</Literal>
			</Declarations>
			<Code Language="cpp"><![CDATA[#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

int main()
{
	
	$selected$$end$

	system("pause");
	return EXIT_SUCCESS;
}]]>
			</Code>
		</Snippet>
	</CodeSnippet>
</CodeSnippets>
  1. 导入
工具 -> 代码片段管理 -> Visual C++
几个常用快捷键
  1. 格式化代码

ctrl k + ctrl f

  1. 注释

ctrl k + ctrl c

  1. 去除注释

ctrl k + ctrl u

  1. 只编译不运行

ctrl shift b

GCC编译的四个步骤

记事本创建hello.c
#include <stdio.h>

#define PI 3.14159

int main(void)
{
    int a = 1;
    #ifdef PI //判断某个宏是否被定义,若已定义,执行随后的语句
    printf("PI: %f\n", PI);
    #endif
    printf("hello c\n");
    return 0;
}
步骤一:预处理
  1. 参数:-E
  2. 命令:gcc -E hello.c -o hello.i
  3. -o 选项:表示重命名
  4. 主要工作
  1. 展开头文件

即,会将所有引入的头文件,如此例中的<stdio.h>中的stdio.h文件进行展开,需要注意的是,只是展开但是不会对头文件进行检查和校验,即写成<test.txt>也一样会将text.txt进行展开。

gcc -E hello.c -o hello.i -I . //需要使用 -I 选项指定test.txt的所在路径
  1. 展开条件编译

即,在此步骤中,条件编译#ifdef部分代码会被展开执行

#ifdef PI
printf("PI: %f\n", PI); 
#endif

执行后的 hello.i

printf("PI: %f\n", PI); 
  1. 替换注释

即,在此步骤中,无论单行注释还是多行注释,都会被替换为空白行

  1. 替换宏定义

即,在此步骤中,宏定义会执行替换工作,如例中的宏变量PI,会被替换为其宏值3.14159

//hello.i
printf("PI: %f\n", 3.14159);
步骤二:编译
  1. 参数:-S
  2. 命令:gcc -S hello.i -o hello.s
  3. 主要工作
  1. 会逐行检查语法,也因此编译过程是最为耗时的过程
  2. 将C语言转换成汇编指令
步骤三:汇编
  1. 参数:-c
  2. 命令:gcc -c hello.s -o hello.o
  3. 主要工作:将汇编指令翻译为机器码(二进制编码)
步骤四:链接
  1. 参数:无
  2. 命令:
gcc hello.o -o hello.exe //windows系统
gcc hello.o -o hello //linux系统,之后 ./hello 即可调用执行
  1. 主要工作
  1. 数据段合并
  2. 数据地址回填
  3. 库引入

即除了头文件外,还需要引入许多其他系统库,可以使用depends.exe查看所引入的库(将hello.exe拖入其中即可)

概念展开
带参数的宏定义

常将一些短小而频繁使用的函数写成宏函数,除了简化代码,方便使用外,宏函数还无需普通函数的参数压栈、跳转、返回等的开销

#define SUM(x, y)(x + y)

int main(void)
{
	int a = SUM(1, 2); //只是做替换,即 1+2,而不做计 算
	printf("%d", a); //3
	system("pause");
	return EXIT_SUCCESS;
}
几个特殊的预定宏
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <Windows.h>
/*
__FILE__	宏所在文件的源文件名
__LINE__	红所在行的行号
__DATE__	代码编译的日期
__TIME__	代码编译的时间
*/

int main(void)
{
	printf("%s\n", __FILE__); //D:\Project\C\demos\learning\mymicro.c
	printf("%d\n", __LINE__); //18
	printf("%s\n", __DATE__); //Oct  5 2021
	printf("%s\n", __TIME__); //17:42:36
	system("pause");
	return EXIT_SUCCESS;
}

条件编译

一般情况下源程序中所有的行都参加编译,但是有时候希望对部分源程序只在满足一定条件时才编译,即对这部分源程序指定编译条件

  1. 测试存在
#ifdef 标识符
    程序段1
#else
    程序段2
#endif
  1. 测试不存在
#ifndef 标识符
    程序段1
#else
    程序段2
#endif
  1. 根据表达式定义
#if 表达式
    程序段1
#else
    程序段2
#endif

指已经写好的、可复用的代码,可以简单看成一组目标文件的集合

静态库的封装与使用
  1. 创建一个空项目:如(staticlib)
  2. mylib.h
#pragma once
int myAdd(int a, int b);
  1. mylib.c
#include "myStaticLib.h"

int myAdd(int a, int b)
{
    return a + b;
}
  1. 右键项目(staticlib)-> 配置属性 -> 常规 -> 项目默认值(配置类型) -> 静态库(.lib)
  2. 右键项目 -> 生成/重新生成
  3. 获取mylib.h(相当于接口说明), staticlib.lib(解决方案/Debug 目录下)两个文件
  4. 右键项目 -> 添加 -> 现有项 -> 选中上面的两个文件(可以不用mylib.h)
  5. 直接使用函数即可int res = myAdd(1, 2);
静态库的优缺点 & 解决方案之动态链接
  • 静态库的优缺点
  1. 静态库对函数库的链接是在编译时期完成的,并且在程序的链接阶段被复制到了程序中,即和程序运行的时候没有关系
  2. 由上可知:由于程序在运行时与函数库再无瓜葛,所以方便移植
  3. 由上可知:因为将整个静态库文件合成到最终的可执行文件中,所以浪费资源(举例:1M的静态库文件,如果有2000个程序引入它,意味着有2G左右的空间被浪费)
  • 解决方案之动态链接

动态链接的基本思想,就是将程序的模块互相分隔开,形成独立的文件,而不是将它们静态地链接在一起,等程序运行时才进行链接,简而言之,就是将链接的过程推迟到了运行时才开始。

动态库的封装与使用
  1. 创建一个空项目:如(dynamicdll)
  2. mydll.h
#pragma once
__declspec(dllexport) int myAdd(int a, int b);
  1. mydll.c
#include "myDynamicDll.h"

int myAdd(int a, int b) //__declspec(dllexport)可加可不加
{
    return a + b;
}
  1. 右键项目(dynamicdll)-> 配置属性 -> 常规 -> 项目默认值(配置类型) -> 动态库(.dll)
  2. 生成/重新生成
  3. 获取dynamicdll.lib(和静态库不同,这里只会存放变量以及导出函数的声明), dynamicdll.dll(包含函数的实现体), myDynamicDll.h三个文件(解决方案/Debug 目录下)
  4. 添加现有项(同静态库操作步骤)将三个文件加入 或者 用以下方式
#pragma comment(lib, "路径/dynamicdll.lib")
int main ...

常量与变量

常量的定义
/*
方式一:宏定义
#define:表示进行宏定义
宏名:PI
宏值:3.1415926
注意1:无封号,也无需类型关键字
注意2:宏名一般用大写
注意3:宏定义可以是常数、表达式等
注意4:宏定义不做语法检查,只有在编译阶段被宏展开后的源程序才会报错
注意5:宏定义的有效范围默认为从定义到本源文件结束
注意6:可以用 #undef 命令结束宏定义的作用域,如 #undef PI
注意7:在宏定义中,可以引用已定义的宏名
*/
#define PI 3.1415926

/*
方式二:const关键字
const关键字:所修饰的变量只读,相当于就是个常量
注意:不推荐用这种方式来创建常量
*/
const int a = 1;
变量的定义
/*变量类型 变量名 = 变量值*/
int var = 1;
变量的声明
/*
方式一:没有变量值的定义为变量的声明
*/
int var;

/*
方式二:extern关键字
*/
extern int a;

/*
两种方式的区别
  对应一个变量,在使用之前必须要进行定义,如果只是声明而为定义,
  则C也会自行将该声明提升为定义,即会赋予一个默认值,
  如果该变量是全局变量,则赋予值0,
  否则如果是linux则赋予一个随机数,如果是windows则直接编译出错;
  而extern的特别之处就在于,用了该关键字后,变量声明无法被提升为定义。
*/
#include <stdio.h>
int a;
int main(void)
{
	printf("a: %d\n", a); //0
	return 0;
}
#include <stdio.h>

int main(void)
{
    int a;
	printf("a: %d\n", a); //windows下直接编译出错
	return 0;
}
#include <stdio.h>
extern int a;
int main(void)
{
	printf("a: %d\n", a); //报错,因为extern修饰后无法被提升
	return 0;
}

数据类型

整形
几种整形
  1. short:2字节
  2. int:4字节
  3. long:windows下4字节;linux下32位系统为4字节,64位为8字节
  4. long long:8字节
有符号与无符号
  1. 关键字
  1. signed:有符号

默认就是有符号,所以通常省略不写

  1. unsigned:无符号

即存储的字节中无符号位

  1. 字节数

有符号和无符号的字节数一样

其他几个常用类型
字符类型
  1. 关键字:char
  2. 字节数:1
  3. 常见字符与其对应的数值
字符数值
\00
\n10
048
A65
a97
  1. char的取值范围
    1. 规定
    00000000: 0; 10000000: -128
    
    1. 有符号
    -2^(8-1) ~ 2^(8-1) - 1; // -128 ~ 127 减1是因为从0开始
    
    1. 无符号
    0 ~ -2^(8) - 1; // 0 ~ 255 减1是因为从0开始
    
浮点类型
  1. 字节数
    1. float:4
    2. double:8
  2. 注意
    float f = 1.1f; //需要加上f后缀,否则默认视为double类型
字符串类型
  • 说明

在C语言中,字符串类型有个很重要的特点,就是\0,这是字符串的结束标记,且对于用户设置的字符串,系统会自动添加该字符,如定义的 “abc”,实质上是4个字符,即"abc\0"

printf("ab\0c"); //ab,当遇到第一个\0时,停止打印
/*printf的特性:例子中ab后输出一堆乱码,原因是printf没有遇到\0,所以会一直向后打印,直到遇到为止*/
char str[2] = { 'a', 'b' };
printf("str: %s\d", str); //str: ab烫烫烫烫烫d

字符串相当于是字符数组的一个特殊形式,即必须以’\0’结尾

  • 定义
/*方式一:使用字符数组来定义,需要手动填写'\0'字符串结束符*/
int str[6] = {'h', 'e', 'l', 'l', 'o', '\0'}; // 如果此处元素个数设置为7个,则第七个元素,即未赋值的元素,默认值为'\0'

/*方式二*/
int str[] = "hello"; //默认就有'\0'

/*定义一个全是'\0'的字符数组*/
char str[6] = {0};
  • 接收
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main(void)
{
	char str1[6] = { 0 };

	/*方式一*/
	scanf("%s", str1);
	printf("res1: %s\n", str1); //hello

	char str2[6] = { 0 };
	/*方式二*/
	for (size_t i = 0; i < sizeof(str1) / sizeof(str1[0]); i++)
	{
		scanf("%c", &str2[i]);
	}
	printf("res2: %s\n", str1); //hello
	return 0;
}
  • 方法
  • gets & fgets
int main(void)
{
	/*
	char *gets(char *str)
	1. 说明:读取整行输入(可以接收空格),直至遇到换行符,然后丢弃换行符,储存其余字符,并在其末尾添加一个空字符使其成为一个字符串。
	2. 问题:gets和scanf一样,未进行检查,所以存在越界问题,即有多少字符它就给你输入多少,所以有可能擦掉程序中的其他数据,对此也有gets_s函数
	3. 返回值:char *,返回一个字符串的首地址,即实际获取到的字符串
	4. 参数str:用来存储字符串的空间
	*/
	char str1[10]; //char str1[11] = {0}; 这样才不会越界
	//gets(str);
	gets_s(str1, 11);
	printf("str1 = %s\n", str1); //helloworld

	/*
	char *fgets(char *str, int size, FILE *stream)
	1. 说明:作用同gets,但是不存在溢出问题,另外会预留'\0'的位置,还有对于最后输入的\n,当空间充足时会读取,否则会舍弃
	2. 返回值:同gets
	3. 参数str:同gets
	4. 参数size:存储空间大小,同gets_s的第二个参数
	5. 参数stream:读取字符串的位置(如可以输入文件路径),键盘为标准输入stdin
	*/
	char str2[11];
	printf("str2 = %s\n", fgets(str2, sizeof(str2), stdin)); //helloworld
	return 0;
}
  • puts & fputs
int main(void)
{
	/*
	int puts(const char *str)
	1. 说明,和get(读取)相反,是用于将字符串写出到屏幕,另外会自动在最后输出\n
	2. 返回值:是否写入成功,-1表示失败,成功为一个非负数(一般用0表示)
	3. 参数str:待写出的字符串
	*/
	char str[] = "hello world";
	int ret1 = puts(str);
	printf("ret1 = %d\n", ret1);

	/*
	int fputs(const char *str, FILE *stream)
	1. stream:可以指定写出的位置,默认为屏幕,即标准输出stdout
	2. 区别:不会自动输出\n
	*/
	int ret2 = fputs(str, stdout);
	printf("ret2 = %d", ret2);
	return 0;
}
  • strlen
int main(void)
{
	/*
	size_t strlen(const char *str)
	1. 说明:用于获取字符串的有效长度,不包含'\0'
	2. 头文件:<string.h>
	3. size_t:unsigned int
	*/
	char str1[] = "hello world";
	printf("sizeof: %u\n", sizeof(str1)); //12
	printf("strlen: %u\n", strlen(str1)); //11
	char str2[] = "hello\0world";
	printf("sizeof: %u\n", sizeof(str2)); //12
	printf("strlen: %u\n", strlen(str2)); //5
	return 0;
}
sizeof & printf
sizeof
  1. 不是个函数,无需引入头文件,作为一个关键字,用于计算一个数据类型的字节大小
  2. 返回值类型为size_t,通过typedefunsigned int起的一个别名
  3. 如果传入的是变量值,则括号可以省略
printf
常见格式匹配符说明
%dsigned int 10进制
%osigned int 8进制
%xsigned int 16进制
%hdsigned short
%ldsigned long
%lldsigned long long
%uunsigned int
%huunsigned short
%luunsigned long
%lluunsigned long long
%cchar
%sstring
%ffloat(默认保留6位小数)
%.nffloat(指定保留n位小数)
%m.nffloat( 整数+1(小数点)+小数n = m ,不足默认用空格左填充)
%0m.nffloat( 不足时用0左填充 )
%lfdouble
%%%
示例
#include <stdio.h>
extern int a;
int main(void)
{
	short s = 2;
	int i = 4;
	long l = 8; // or 8L
	long long ll = 8; // or 8LL
	unsigned short us = 2; // or 2u
	unsigned int ui = 4; // or 4u
	unsigned long ul = 8; // or 8Lu
	unsigned long long ull = 8; // or 8LLU

	printf("short: %u\n", sizeof s); // 2
	printf("int: %u\n", sizeof(int)); // 4
	printf("long: %u\n", sizeof(long)); // 4
	printf("long long: %u\n", sizeof(long long)); // 8
	printf("unsigned short: %u\n", sizeof(unsigned short)); // 2
	printf("unsigned int: %u\n", sizeof(unsigned int)); // 4
	printf("unsigned long: %u\n", sizeof(unsigned long)); // 4
	printf("unsigned long long: %u\n", sizeof(unsigned long long)); // 8

	return 0;
}
类型限定符
extern

表示 变量/函数 声明,用该关键字声明后的 变量/函数 将不会自动创建内存存储空间

volatile

用于阻止编译器自行优化代码

volatile int flag = 0;
flag = 1; //1
flag = 0; //2
flag = 1; //3
flag = 0; //最后依旧是flag=0,对于这种情况,编译器会进行代码优化,会删除中间变化,即1,2,3步的代码会被删除,而使用了volatile后将不会删除
const

见常量的定义

register

定义一个寄存器变量,即用该关键字定义的变量,将直接存放到寄存器中,但要注意的是不一定保证每次都存放成功,如寄存器已满的情况下将存放失败

寄存器没有和内存一样的地址的概念

int a = 5; //默认将a放入内存中
a + 10; //当使用a时,会先将a放入寄存器中,然后开始执行相关运算

数值存储方式

存储方式

计算机内部以补码的方式存储一个数值

原码
  • 特点
  1. 最高位作为符号位,0正1负
  2. 其他部分为该数值本身绝对值的二进制表示
  • 示例
数值原码
1500001111
-1510001111
000000000
-010000000
反码
  • 特点
  1. 正数的反码同原码
  2. 负数的反码为原码符号位不变,其他位置取反
  • 示例
数值反码
1500001111
-1511110000
000000000
-011111111
补码
  • 特点
  1. 正数的补码同原码
  2. 负数的补码为反码符号位不变,加 1
  • 根据补码求原码
  1. 符号位不变,补码 - 1,然后取反
  2. 符号位不变,取反,再加 1
  • 示例
数值反码
1500001111
-1511110001
000000000
-010000000
示例:43 - 27

+43的补码:00101011
-27的原码:10011011
-27的反码:11100100
-27的补码:11100101

00101011
11100101
——————
00010000 = 16

数据溢出
符号位溢出
  • 说明

对于有符号的数值可能发生,发生后数值的正负符号将改变

  • 示例
char c = 127 + 1;
01111111
00000001
-------------
10000000(补码)
11111111(反码)
10000000(原码) = -128
最高位溢出
  • 说明

对于无符号的数值可能发生,由于最高位丢失,所以发生后数值的差异可能十分大

  • 示例
unsigned char c = 255 + 1;
11111111
00000001
--------------
100000000 -> 溢出后:00000000 = 0

运算符

算数运算符

同java

赋值运算符

同java

比较运算符

同java

逻辑运算符

同java

  • 注意

c中 0、’\0’ 也表示false

	    int aa = 0;
		if (!aa) 
		{
			printf("aa0 = %d", aa);
		}
		else 
		{
			printf("aa1 = %d", aa);
		}
		//aa0 = 0
三目运算符

同java

逗号运算符
int a = 10, b, c = 30;
int x = (a = 100, b = a * 2, c = c * 10 + a + b);
printf("a = %d\n", a); //100
printf("b = %d\n", b); //200
printf("c = %d\n", c); //600
printf("x = %d\n", x); //600,取最后一个结果
return 0;

类型转换

隐式类型转换

注意:如 float、int 进行运算时,都转为链中的交点,即double

char, short
signed int
unsigned int
long
double
float
强制类型转换

同java

分支语句

同java

循环语句

同java

数组

  1. 相同数据类型的有序(地址)集合
  2. 数组名为第一个元素的地址
  3. 数组空间大小为所有元素大小之和
#include <stdio.h>


int main(void)
{
	int arr[5] = { 4, 6, 1, 2, 90 };
    printf("arr = %x\n", arr);
	printf("&arr[0] = %x\n", &arr[0]); //   地址为16进制,所以可以用%x来输出打印
	printf("&arr[1] = %x\n", &arr[1]); //   
	printf("&arr[2] = %p\n", &arr[2]); //   %p为地址的格式匹配符
	printf("&arr[3] = %p\n", &arr[3]); //   地址为16进制,所以可以用%x来输出打印
	printf("&arr[4] = %p\n", &arr[4]); //   地址为16进制,所以可以用%x来输出打印
	printf("数组元素大小:%u\n", sizeof(int)); //4
	printf("数组大小:%u\n", sizeof(arr)); //20,即 (4 * 5)
	/*
   arr = 10ffc48
	&arr[0] = 10ffc48
	&arr[1] = 10ffc4c
	&arr[2] = 010FFC50
	&arr[3] = 010FFC54
	&arr[4] = 010FFC58
	可见,彼此相隔4字节,是连续的地址
	*/
	return 0;						      
}

  1. 初始化一个全为0的数组,需要设置至少一个0元素,否则将是随机数
int arr1[10]; //生成的是10个随机数
int arr2[10] = {0}; //生成全为0的数组,即同java一样,未设置的元素默认值为0
int arr3[3][5] = {0}; //多维数组也一样
//注意
int arr[10];
arr[0] = 1;
//这样做,arr[1 ~9]依旧是个随机数,不是0,因为在声明时已经赋予了初始随机默认值
  1. 可以不指定个数,即数组可以自己计算元素个数
int arr[] = {1, 2, 3}; //会自动统计元素个数 - 3

函数

函数声明
返回值类型 函数名(形参列表); //这就是函数声明
隐式函数声明
/*
1. 在 c 语言中,在调用一个函数之前,必须先有该函数的定义,
2. 如果没有则必须有函数声明,如果依旧没有,则编译器自动为该函数添加一个隐式的函数声明
3. 只不过隐式声明的函数返回值,均为 int 类型
*/
函数声明案例
  1. 函数调用前有函数定义:可以正常调用
#include <stdio.h>

void add(int a, int b) 
{
	printf("a + b = %d", a + b); //a + b = 3
}
int main(void)
{
	add(1, 2);
	return 0;
}
  1. 函数调用前无函数定义:自动隐式声明

如果函数恰好返回值为int,则正常调用,否则将抛出重复声明定义的错误

  1. 函数调用前无函数定义,但是有函数声明:可以正常调用
#include <stdio.h>

void add(int a, int b); //函数声明

int main(void)
{
	add(1, 2);
	return 0;
}

void add(int a, int b)
{
	printf("a + b = %d", a + b); //a + b = 3
}

exit函数
  1. 头文件

stdlib

  1. exit()

退出当前程序

  1. return

底层实质上在调用_exit()方法

main函数
不带参的main函数
  • 语法
//写法一
int main(void);

//写法二
int main();
带参的main函数
  • 语法
//写法一
int main(int argc, char * argv[]);

//写法二
int main(int argc, char ** argv);

/*
argc: 传递的参数的个数 
argv: 指针数组,其中数组的每个元素为char *,即字符串
 */
  • 测试
#include<stdio.h>

int main(int argc, char* argv[])
{
	for (size_t i = 0; i < argc; i++)
	{
		printf("argv[%d] = %s\n", i, argv[i]); //test.exe aa bb cc dd ee,即至少有一个参数:argv[0] = 程序的相对路径.exe
	}
	return 0;
}
  • 测试一:使用gcc
1. gcc test.c -o test.exe
2. test.exe aa bb cc dd ee
  • 测试二:利用vs
右键项目 -> 属性 -> 调试 -> 命令参数
函数与指针
  • 栈帧

当函数调用时,系统会在stack空间中申请一块栈帧内存区域,可以存放形参与局部变量,以用来提供函数调用


当函数调用时,这块栈帧内存区域也会被自动释放

  • 传值
  • 传递非地址
#include<stdio.h>

/*
1.在stack中开辟main栈帧,存储局部变量m和n
2.在stack中,挨着main再创建一个swap栈帧,存储形参a和b,再存储局部变量tmp
3.swap中a和b的值交换
4.swap结束,对应栈帧释放
5.输出m和n,依旧是原来的值
*/

void swap(int, int);

int main(void)
{
	int m = 1; 
	int n = 2;
	printf("m = %d, n = %d\n", m, n); //m = 1, n = 2
	swap(m, n);
	printf("m = %d, n = %d\n", m, n); //m = 1, n = 2
	return 0;
}

void swap(int a, int b)
{
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
}
  • 传递地址
#include<stdio.h>

void swap(int* a, int* b)
{
	int tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
}

int main(void)
{
	int m = 1;
	int n = 2;
	printf("m = %d, n = %d\n", m, n); //m = 1, n = 2
	swap(&m, &n);
	printf("m = %d, n = %d\n", m, n); //m = 2, n = 1
	return 0;
}
  • 传递数组
    当数组作为参数时,传递给函数的不是整个数组,而是数组的首地址,所以此时在函数中对形参进行sizeof计算,得到的是一个指针的大小,而非整个数组的大小,也因此,当需要传递数组时,通常会额外的添加一个形参,用于接收数组的长度
#include<stdio.h>

//形参也可以指定长度,即int arr[100],
//形参还可以直接用指针,即int * arr
void bubbleSort(int arr[], int length) 
{
	//printf("sizeof(arr) = %d", sizeof(arr)); //4,求的是一个int类型指针的大小
	int* inner;
	for (size_t i = 0; i < length - 1; i++)
	{
		inner = arr;
		for (size_t j = 0; j < length - i - 1; j++, inner++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp;
				tmp = *inner;
				*inner = *(inner+1);
				*(inner + 1) = tmp;
			}
		}
	}
}

int main(void)
{
	int arr[10] = { 2, 5, 9, 1, 4, 3, 7, 6, 10, 8 };
	int length = sizeof(arr) / sizeof(arr[0]);
	bubbleSort(arr, length);
	for (size_t i = 0; i < length; i++)
	{
		printf("arr[%d] = %d \n", i, arr[i]); // 1 2 3 4 5 6 7 8 9 10
	}
	return 0;
}
  • 返回值
  • 返回数组

C语言不允许返回数组

  • 返回指针

不要返回函数内部的局部变量,因为函数结束后栈帧被释放,其中的局部变量将随时被系统分配到其他地方使用

#include<stdio.h>

int m = 10000;

int* ret_point_test()
{
	/*
	不要直接返回局部变量
	int a = 100;
	return &a;
	*/
	return &m;
}

int main(void)
{
	int* p;
	p = ret_point_test();
	printf("p = %d\n", *p); //10000,如果返回的是局部变量a,输出的可能是100,那是因为ret_point_test对应的栈帧结束并被标记为释放后,暂时没有被系统分配给其他地方使用
	return 0;
}
函数指针
  • 示例(c++)
#include "mainwindow.h"
#include <QApplication>
#include <QDebug>

void func()
{
	qDebug("hello world");
}

int main(int argc, char *argv[])
{
	QApplication a(argc, argv);
	qDebug("%d", func); //10555457,在qt中,该值重启后还是一样的
	void(*pFunc)(int) = (void(*)(int))10555457; //将地址转为函数指针
	pFunc(1); //通过函数指针调用函数

	return a.exec();
}

  • 函数指针的定义方式
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

void func(int a, char* c)
{
	printf("%d: hello function pointer from %s\n", a, c);
}

void method1()
{
	//1.先定义函数类型(类型名称一般用全大写)
	typedef void(FUNC_M1)(int, char*);
	//2.再定义函数指针变量
	FUNC_M1 *m1 = func;
	//3.调用
	m1(1, "m1");
}

void method2()
{
	//1.先定义函数指针类型
	typedef void(*FUNC_M2)(int, char*);
	//2.再定义函数指针变量
	FUNC_M2 m2 = func;
	//3.调用
	m2(2, "m2");
}

void method3()
{
	//直接定义函数指针变量
	void(*m3)(int, char*) = func;
	m3(3, "m3");
}

int main(void)
{

	/*方式一*/
	method1();

	/*方式二*/
	method2();

	/*方式三*/
	method3();

	system("pause");
	return EXIT_SUCCESS;
}

  • 函数指针数组
int main(void)
{

	void(*arr[3])();
	arr[0] = method1;
	arr[1] = method2;
	arr[2] = method3;
	for (size_t i = 0; i < 3; i++)
	{
		arr[i]();
	}

	system("pause");
	return EXIT_SUCCESS;
}
回调函数

即函数指针作为函数参数

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

void arrayPrinter(void* array, int size, int length, void(*printer)(void*))
{
	char* addr = array;
	char* eleAddr;
	for (size_t i = 0; i < length; i++)
	{
		//获取每个元素的首地址
		eleAddr = addr + size * i;
		printer(eleAddr);
	}
}

typedef struct Demo
{
	char desc[64];
	int code;
} Demo;

void structPrinter(void* addr)
{
	Demo* demo = (Demo*)addr;
	printf("desc=%s, code=%d\n", demo->desc, demo->code);
}

int main(void)
{
	Demo demos[] = {
		{"福建", 1},
		{"江苏", 5},
		{"山东", 3}
	};
	int size = sizeof(Demo);
	int length = sizeof(demos) / size;
	arrayPrinter(demos, size, length, structPrinter);
	system("pause");
	return EXIT_SUCCESS;
}

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

void sort(void* array, int size, int length, int(*comparer)(void*, void*))
{
	if (!array) return;
	char* temp = malloc(size);
	if (NULL == temp) return;
	char* minOrMaxAddr;
	char* comparerAddr;
	for (size_t i = 0; i < length; i++)
	{
		for (size_t j = 0; j < length - i -1; j++)
		{
			minOrMaxAddr = ((char*)array) + j * size;
			comparerAddr = minOrMaxAddr + size;
			if (comparer(minOrMaxAddr, comparerAddr)) {
				memcpy(temp, minOrMaxAddr, size);
				memcpy(minOrMaxAddr, comparerAddr, size);
				memcpy(comparerAddr, temp, size);
			}
		}
	}
}

int intComparer(void* prevData, void* data)
{
	int* data1 = prevData;
	int* data2 = data;
	return *data1 > *data2;
}

typedef struct Persion
{
	int age;
	char* name;
} Persion;

int persionComparer(void* prevData, void* data)
{
	Persion* p1 = prevData;
	Persion* p2 = data;
	return p1->age < p2->age;
}


int main(void)
{
	int arr[10] = { 2, 5, 3, 1, 4, 6, 9, 10, 8, 7 };
	int length = sizeof(arr) / sizeof(arr[0]);
	sort(arr, sizeof(int), length, intComparer);
	for (size_t i = 0; i < length; i++)
	{
		printf("%d\n", arr[i]);
	}

	Persion persions[] = {
		10, "小明",
		21, "张三",
		19, "小红"
	};
	sort(persions, sizeof(Persion), 3, persionComparer);
	for (size_t i = 0; i < 3; i++)
	{
		printf("name=%s, age=%d\n", persions[i].name, persions[i].age);
	}
	system("pause");
	return EXIT_SUCCESS;
}

递归函数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <Windows.h>

void stringReversePrint(char* string)
{
	if ('\0' == *string) return;
	stringReversePrint(string + 1);
	printf("%c\n", *string);
}

int main(void)
{
	stringReversePrint("abcdef");
	system("pause");
	return EXIT_SUCCESS;
}

多文件编程

  • 含义

将多个含有不同功能.c文件模块,编译到一起,生成一个可执行文件

  • 源文件/add.c
int add(int a, int b)
{
	return a + b;
}
  • 头文件/demo.h
  1. 头文件守卫

用于防止同一头文件在单个CPP/C文件中被重复分析

//方式一:仅在windows系统中可以使用
#pargma once

//方式二:通用方法
#ifndef __DEMO_H__ //对应demo.h
#define __DEMO_H__
    //头文件内容
#endif
  1. 头文件内容
/*
常用内容如下:
1. 头文件引入,如 #include <stdio.h>
2. 函数声明
3. 类型定义
4. 宏定义
 */
#ifndef __DEMO_H__
#define __DEMO_H__
//引入头文件
#include <stdio.h>
//函数声明
int add(int a, int b);
//类型定义
//宏定义
#endif
  • 源文件/main.c
#include "demo.h" //自定义头文件的引入用"",在预处理时将会被展开

int main(void)
{
	int res = add(1, 2);
	printf("1 + 2 = %d", res);
	return 0;
}

指针

指针和内存单元
  1. 指针

即内存地址

  1. 内存单元

计算机中内存的最小存储单位,大小为一个字节

  1. 内存单元与指针的关系

每个内存单元都对应有一个惟一编号,该编号就称为该内存单元的地址

  1. 指针变量

即存储指针的变量

指针的定义和使用
  • 关于变量在等号左右两边的不同含义
int m = 10; //m在=左边,表示的是变量名,即对应的内存空间
int n = 20;
n = m; //m在=右边,表示的是m所指向的内存空间中的存储的内容,即10
int a = 10;
int *p = &a; //定义一个整形指针变量p,注意也可以写成 int* p 或 int * p
//等价于 int *p; p = &a;
*p = 20; //指针的使用(指针的解引用/简介引用)
printf("a = %d", a); //20
/*
 * 解释
 * 1. 开辟空间,存储变量a,内容为10
 * 2. 开辟空间,存储变量p,内容为a的地址
 * 3. 取出p变量的内容,当做地址看待,并找到地址对应的内存空间(即a的内存空间),此时:
 * 		1. 如果*p为左值(即在等号左边),则将数据(20)存储到内存空间中
 * 		2. 如果是右值,则取出空间中的内容
 */
 int aa = 10;
 int *pp = &aa;
 aa = 20;
 printf("*pp = %d", *pp); //*pp取出aa的内容,所以为20
指针的大小

指针的大小与数据类型无关,只与操作系统位数有关,即32位的系统,指针为4字节;64位操作为8字节

printf("%u\n", sizeof(void*));
printf("%u\n", sizeof(char*));
printf("%u\n", sizeof(short*));
printf("%u\n", sizeof(int*));
printf("%u\n", sizeof(long*));
printf("%u\n", sizeof(long long*));
printf("%u\n", sizeof(float*));
printf("%u\n", sizeof(double*));
野指针与空指针
  1. 没有一个有效空间地址的指针称为为野指针
int main(void)
{
	int* p;
	*p = 10000; //将p变量的内容取出当做一个地址看待,但是内容是个随机数,所以会报错
	return 0;
}
  1. p,即指针有一个值,但是该值是不可访问的,则此时p也是个野指针
int main(void)
{
	int* p = 10; //定义了一个指针变量p,地址为10,但是【内存地址0-255规定是要留给操作系统使用的,即不可访问】,所以会报错,要注意的是即使不是0-255,指定的数值地址,也不一定是可访问的
	*p = 10000;
	return 0;
}
  1. 杜绝使用野指针 – 空指针
int main(void)
{
	int* p = NULL; //此时p为定义的一个空指针【NULL = (void *)0,即0地址 --> 是不可访问的】
    //...
	if (p != NULL)
	{
		*p = 10000;
	}
	return 0;
}
泛型指针

可以接收任意一种变量地址,但是在使用的时候必须进行强制类型转换,转换为对应的数据类型

int main(void)
{
	void* p1, * p2;
	int a = 10;
	char ch = 'R';
	p1 = &a;
	p2 = &ch;
	printf("p1 = %d\n", *((int *)p1));
	printf("p2 = %c\n", *((char *)p2));
	return 0;
}
指针和数组
  • 数组名是地址常量,即不可被修改
  • &arr = arr
  • 指针是变量,所以可以用数组名给指针赋值
  • 取数组元素的几种方式
int main(void)
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int* p = arr;
	int length = sizeof(arr) / sizeof(arr[0]);
	for (size_t i = 0; i < length; i++)
	{
		//方式一:arr[i]
		printf("方式一:%d\n", arr[i]);
		//方式二:*(arr+i)
		printf("方式二:%d\n", *(arr + i));
		//方式三:p[i]
		printf("方式三:%d\n", p[i]);
		//方式四:*(p+i)
		printf("方式四:%d\n", *(p + i));
	}
	return 0;
}
  • 指针和数组的区别
  1. 指针是变量,数组名为常量
  2. sizeof
sizeof(指针) //4/8
sizeof(数组) //数组的实际字节数
指针与const
  • 通过指针的方式可以修改const修饰的变量的值
int main(void)
{
	const int a = 10;
	int *p = &a;
	*p = 20;
	printf("a = %d\n", a); //20
	return 0;
}
  • const对指针的几种修饰方式
int main(void)
{
	/*
	1. const int *p
	2. int const *p
	可以修改p变量,但是不能修改*p

	3. int * const p
	不可以修改p变量,但是可以修改*p
	4. const int * const p
	p变量,*p都不可以被修改
	*/
}
  • const修饰指针的应用场景

常用语函数形参,如

int fputs(const char * str, FILE * stream);
防止待输出的字符串str被中途篡改
指针类型的作用

如有数据:

int a = 0x12345678;

其在内存的存储情况如下:

1. int类型共占有4个字节,对应四个连续的地址0xff000xff03,并以首地址0xff00为变量a地址
2. 一个16进制对应4个二进制位,所以两个16进制数就
78
56
34
12
0xff00
0xff01
0xff02
0xff03

指针类型的作用:

  1. 决定了指针从存储地址开始的读取的字节数,如int一次读取4个字节
  2. 决定了指针变量做加/减1运算时偏移的字节数,如int一次偏移4个字节
int main(void)
{
	int a = 0x12345678;
	int* p1 = &a;
	short* p2 = &a;
	char* p3 = &a;
	printf("*p1 = %p\n", *p1); //12345678
	printf("*p2 = %p\n", *p2); //00005678
	printf("*p3 = %p\n", *p3); //00000078
    printf("&a = %p\n", &a); //00FF0000
	printf("p1 = %p\n", p1); //00FF0000
	printf("p1 + 1 = %p\n", p1 + 1); //00FF0004
	printf("p2 = %p\n", p2); //00FF0000
	printf("p2 + 1 = %p\n", p2 + 1); //00FF0002
	return 0;
}
int main(void)
{
	int arr[] = {1, 2, 3, 4, 5, 6, 7, 8 ,9};
	int* p = &arr;
	int length = sizeof(arr) / sizeof(arr[0]);
	for (size_t i = 0; i < length; i++, p++)
	{
		printf("arr[%d] = %d\n", i, *p);
	}
	printf("p - arr = %d", p - arr); //9,地址相减得到的是元素的个数
	return 0;
}
指针的算术运算
  • 指针与整数的乘除取余操作:error,即无法进行
  • 指针与整数的加减操作
  1. 普通指针变量的加减
char * p; p + 1; //表示偏移对应的字节数
  1. 在数组中的加减
int arr[] = {1, 3, 5};
int *p = arr;
p + 1; //表示偏移的元素个数(本质还是字节数)
  • &数组名 + 1
int main(void)
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8 ,9 };
	printf("arr      = %p\n", arr);
	printf("&arr[0]  = %p\n", &arr[0]);
	printf("arr + 1  = %p\n", arr + 1);
	printf("&arr[1]  = %p\n", &arr[1]);
	printf("&arr     = %p\n", &arr); //数组首元素地址
	printf("&arr + 1 = %p\n", &arr + 1); //偏移一个数组的大小,即偏移(数组元素个数*sizeof(数组类型))的大小
	/*
	arr      = 006FFA38
	&arr[0]  = 006FFA38
	arr + 1  = 006FFA3C
	&arr[1]  = 006FFA3C
	&arr     = 006FFA38
	&arr + 1 = 006FFA5C
	*/
	return 0;
}
  • 指针之间的算术运算
  1. 加、乘除、取余:error,即禁止操作
  2. 减法
int main(void)
{
	//1.对于普通变量,无实际意义
	int a = 10;
	int b = 20;
	int* pa = &a;
	int* pb = &b;
	printf("pa = %p\n", pa); //012FF77C
	printf("pb = %p\n", pb); //012FF770
	printf("pa - pb = %p\n", pa - pb); //00000003,即 (C-0)/sizeof(int)

	//2.对于数组,相当于二者对应元素的下标之差
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int* p = &arr[3];
	int* q = &arr[6];
	printf("q - p = %d", q - p); //3
	return 0;
}
指针的比较运算
  • 地址之间可以进行比较大小(> < =)
  • 普通变量的比较没有意义,主要用于数组,可以将地址视为数字,即比较这个数字的大小,大的表示在更高位
  • p != NULL,即常用语是否为空指针的比较
指针实现的strlen函数
int mystrlen(const char str[]);

int main(void)
{
	char str[] = "hello world";
	int len = mystrlen(str);
	printf("length is %d\n", len);
	return 0;
}

int mystrlen(const char str[]) {
	char* p = str;
	while(*p != '\0') 
	{
		p++;
	}
	return p - str;
}
指针数组
  1. 指针数组本质就是一个二级指针:**arr
int main(void)
{
	int a = 10;
	int b = 20;
	int c = 30;
	int* pa = &a;
	int* pb = &b;
	int* pc = &c;
	int* arr[] = { pa, pb, pc };
	printf("*(arr[0]) = %d\n", *(arr[0])); //10
	printf("*(*(arr + 0)) = %d\n", *(*(arr + 0))); //10
	printf("**arr = %d\n", **arr); //10
	return 0;
}
  1. 二维数组本质也是个二级指针
int main(void)
{
	int a[] = { 10 };
	int b[] = { 20 };
	int c[] = { 30 };
	int* arr[] = { a, b, c };
	printf("arr[0][0] = %d\n", arr[0][0]); //10
	printf("*((*(arr + 0) + 0)) = %d\n", *((*(arr + 0) + 0))); //10
	printf("**arr = %d\n", **arr); //10
	return 0;
}
多级指针
int main(void)
{
	int a = 10;
	int* p = &a; //一级指针是变量地址
	//int **p2 = &(&a) 不允许直接跳级定义
	int** p2 = &p; //二级指针是一级指针地址
	int*** p3 = &p2; //三级指针是二级指针地址
	p3 == &p2;
	*p3 == p2 == &p; //*(&p2) == p2
	**p3 == *p2 == p == &a;
	***p3 == **p2 == *p == a;
	return 0;
}

字符串

指针与字符串
  • 字符串的几种定义方式以及区别
char str1[] = {'h', 'i', '\0'};
char str2[] = "hi";
char * str3 = "hi";
//不允许:char * str = {'h', 'i', '\0'};

//区别一:char *定义的相同值字符串地址相同
char * str = "hi"; //地址和str3的地址一样

//区别二:char *定义的字符串时常量,不允许修改
str1[0] = "H"; //可以
str3[0] = "H"; //是一个常量,不可以修改

//字符串 或 字符数组 和普通数组相比的特性:不需要额外提供一个长度的参数,因为每个字符串都有一个'\0'作为结束标记

//注意:直接写 "字符串" 相当于char*的形式,即也是个常量
printf("abc"); //abc也是个常量
常用字符串函数
strcmp
  • 作用

比较两个字符串大小,即比较两个字符串的每个字符,从第一个字符开始,如果一样则继续比下一个,最终都一样则返回0,如果不一样,则比较此时字符的ascii值,前者大则返回1,否则返回-1

  • 实现
#include<stdio.h>

int myStrcmp(const char* str1, const char* str2)
{
	while (*str1 == *str2)
	{
		if ('\0' == *str1) return 0;
		str1++;
		str2++;
	}
	return *str1 > * str2 ? 1 : -1;
}

int main(void)
{
	char* str1 = "hello109213jkl";
	char* str2 = "hello12";
	int ret = myStrcmp(str1, str2);
	if (ret == 0) printf("str1 == str2");
	else if (ret > 0) printf("str1 > str2");
	else printf("str1 < str2");
	return 0;
}
strncmp
  • 语法
int myStrcmp(const char* str1, const char* str2, size_t n);
  • 作用

strcmp,但是可以指定只比较前n个字符

strcpy
  • 作用

将源字符串的内容拷贝到目标字符串中

  • 实现
#include<stdio.h>

void myStrcpy(const char* src, char* dst)
{
	while (*src) //或者 *src != 0 或者 *src != '\0'
	{
		*dst = *src;
		src++;
		dst++;
	}
	*dst = '\0';
}

int main(void)
{
	char* src = "hell0 world.";
	char dst[15];
	myStrcpy(src, dst);
	printf("dst = %s", dst);
	return 0;
}
  • 缺点

dest的存储空间存在小于src空间的情况,因此此方法是非安全的,对此,可以使用安全的 strncpy 方法

strncpy
  • 作用

strcpy

  • 语法
char* strncpy(char* dest, const char* src, size_t n);
//n: 无符号整数,表示拷贝的字节数,通常设置为dest的长度。
//注意1:不会自动添加\0结束符
//注意2:n如果超过src的长度,也只会拷贝src长度的字符
strstr
  • 语法
char* strstr(char* str, char* substr); //<string.h>
  • 作用

获取substr在str中首次出现的位置到str末尾 的 这部分字符串,若不存在,则返回null

  • 使用
#include<stdio.h>
#include<string.h>

int main(void)
{
	char* res = strstr("helloabc", "ll");
	printf("res = %s\n", res); //lloabc
	return 0;
}
  • 计算子串出现的次数
#include<stdio.h>
#include<string.h>

int substrCount(const char* str, const char* substr)
{
	int res = 0;
	char* p = strstr(str, substr);
	while (NULL != p) {
		res++;
		p += strlen(substr);
		p = strstr(p, substr);
	}
	return res;
}

int main(void)
{
	int res = substrCount("helloworld", "l");
	printf("res = %d\n", res); //3
	return 0;
}
strchr
  • 语法
char* strchr(char* str, int c);
  • 作用

同strstr,只不过找的是一个字符

  • 示例
#include<stdio.h>
#include<string.h>

int main(void)
{
	char* res = strchr("hello", 'l');
	puts(res); //llo
	return 0;
}
strrchr
  • 语法

strchr

  • 作用

同strstr,只不过从右边开始找

  • 示例
#include<stdio.h>
#include<string.h>

int main(void)
{
	char* res = strrchr("hello", 'l');
	puts(res); //lo
	return 0;
}
strcat
  • 语法
char* strcat(char* dest, const char* src);
  • 作用

将src拼接到dest后面,并返回拼接后的结果

  • 问题

(char* dest[10])存在空间不足的情况,所以需要用户使用时自行确保空间充足

strncat
  • 语法
char* strncat(char* dest, const char* src, size_t n);
  • 作用

strcat,但可以指定只拼接的src的前n个长度的字符

  • 问题

strcat

sprintf
  • 语法
int sprintf(char* str, const char* format, ...);
  • 作用

相当于java的String.format,最终结果存入到第一个参数str中

  • 示例
#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<string.h>

int main(void)
{
	//char* str = "a"; //是个常量
	char str[6] = {0}; //必须保证空间足够,否则报错
	sprintf(str, "%d%c%d=%d", 1, '+', 1, 1 + 1);
	puts(str);
	return 0;
}
sscanf
  • 语法
int sscanf(const char* str, const char* format, ...);
  • 作用

相较于scanf,将原来从屏幕输入的格式化字符串,变为从第一个参数str中获取

  • 示例
#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<string.h>

int main(void)
{
	char str[] = "1+1=2";
	int a, b, c;
	int res = sscanf(str, "%d+%d=%d", &a, &b, &c);
	printf("a = %d\n", a); //1
	printf("b = %d\n", b); //1
	printf("c = %d\n", c); //2
	return 0;
}
strtok
  • 语法
char* strtok(char* str, const char* delim);
/*
 str:待分割的字符串,第一次切分时需要传入,后续切分只要传入NULL即可,因为函数在切分后,会将delim之后的字符串保存(静态局部变量),后续如果判断str为NULL时,就会将保存的字符串作为此次用来切分的str
 demim:分隔符,可以传多个,如需要分割.和:,只需".:"即可
 */
  • 作用

字符串分割,函数会找到str中首次出现的delim中的任意一个字符,并将原串,即str中该字符替换为\0,然后返回str的首地址,这样接得到str首地址到\0 的这部分 字符串了

  • 示例
#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<string.h>

int main(void)
{
	//strtoc("abc.com", "."); 不可以,因为这样直接传入的"abc.com"是个常量
	char str[] = "abc.com.cn";
	char* res = strtok(str, ".");
	printf("res = %s\n", res); //abc
	return 0;
}
  • 进阶

“abc de f.com%100.cn p”,获取abc de f com 100 cn p

#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<string.h>

int main(void)
{
	char* res[50] = {NULL}; //需要初始化,否则是野指针,值为默认的随机数字,而该数字对应的地址上可能是禁止访问的,所以在读取时会报错
	char** p = &res;

	char str[] = "abc de f.com%100.cn p";
	char* ret = strtok(str, ". %"); //第一次切分需要传入str
	*p = ret;
	p++;

	while (NULL != ret)
	{
		ret = strtok(NULL, ". %");
		*p = ret;
		p++;
	}
	
	for (size_t i = 0; i < 10; i++)
	{
		if (NULL != res[i])
		{
			printf("res[%d] = %s\n", i, res[i]); 
		}
	}
	/*
	res[0] = abc
	res[1] = de
	res[2] = f
	res[3] = com
	res[4] = 100
	res[5] = cn
	res[6] = p
	*/
	return 0;
}
atoi & atof & atol
  • 语法与作用
int atoi(const char* nptr); //将字符串转为int
float atof(const char* nptr); //将字符串转为float
long atol(const char* nptr); //将字符串转为long
  • 示例
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main(void)
{
	char* str1 = "123";
	int num1 = atoi(str1);
	printf("num1 = %d\n", num1);

	char* str2 = "0.123f";
	float num2 = atof(str2);
	printf("num2 = %.2lf\n", num2); //注意要加上头文件stdlib.h,否则0.00

	char* str3 = "123L";
	long num3 = atol(str3);
	printf("num3 = %ld\n", num3);

	return 0;
}

内存管理

作用域

c语言将变量的作用域分为三种

  1. 代码块作用域(即{}范围内的)
  2. 函数作用域
  3. 文件作用域
局部变量
  • 说明

也叫auto自动变量(auto可写可不写),一般情况下,代码块{}内部定义的变量都是自动变量

  • 特点
  1. 在一个函数内定义,只在该函数范围内有效
  2. 在复合语句中定义,只在该复合语句中有效
  3. 随着函数调用的结束或复合语句的结束,局部变量的生命周期也随之结束
  4. 如果没有被赋予初值,系统会赋予一个随机值
  • 生命周期

从定义变量开始,到所属函数对应的栈帧被释放为止

全局变量
  • 特点
  1. 定义在函数外,可以被本(.c)文件,以及其他文件中的函数共同使用(注意,如果是其他文件中的函数调用,需要使用extern关键字声明)
  2. 全局变量的生命周期同程序的运行周期
  3. 不同文件的全局变量也不可以重名
  4. 如果没有被赋予初值,int类型的赋予0值,其他类型类似
  • 生命周期

从程序执行开始(在main函数之前),到程序执行结束为止,即整个程序执行期间

static局部变量
  • 说明

作用域依旧是局部代码块,但是只会定义一次,且定义的位置是在全局位置,通常用来做计数器

  • 示例
#include<stdio.h>

void test()
{
	static int n = 0;
	printf("n = %d\n", ++n); //1 2 3 4 5
}

int main(void)
{
	for (size_t i = 0; i < 5; i++)
	{
		test();
	}
	return 0;
}
  • 生命周期

从程序执行开始(在main函数之前),到程序执行结束为止,即整个程序执行期间

static全局变量

static int a = 10;
也是定义在函数外,但是经过static关键字修饰后,本文件中的全局变量a,只限制于在本文件中使用,即在其他文件中无法通过extern关键字进行声明与使用。

  • 生命周期

从程序执行开始(在main函数之前),到程序执行结束为止,即整个程序执行期间

extern全局变量声明

extern int a;
只是声明一个变量,该变量(a)已经在别的文件中定义过了,这里只是声明的作用

全局函数与静态函数
  • 说明

在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,意味着该函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数。

  • 生命周期

无论是全局函数还是静态函数,也一样都是从程序执行开始(在main函数之前),到程序执行结束为止,即整个程序执行期间

内存模型
windows(四驱模型)

在这里插入图片描述

linux

.text 和 .rodata 链接后会合并,赋予只读权限
.data 和 .bss 链接后会合并,赋予读写权限
在这里插入图片描述

常用函数
memset
  • 说明
void *memset(void *s,int c,size_t n)
/**
 * 将已开辟内存空间 s 的首 n 个【字节】的值设为值 c
 **/
  • 示例
#include<stdio.h>
#include<string.h>

int main(void)
{
	//对应int数组需要注意
	int arr[] = { 1, 2, 3 };
	memset(arr, 1, 3); 
	/*
	表示对arr的前三个字节设为1,而一个int类型对应有4字节,
	所以数字的第一个元素为:00000000 00000001 00000001 00000001
	即:1 + 2^8 + 2^16 = 65793
	*/
	for (size_t i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("arr[%d] = %d \n", i, arr[i]);
		/*
		arr[0] = 65793
		arr[1] = 2
		arr[2] = 3
		*/
	}

	//一般用于字符数组(字符串)
	char str1[] = { 'a', 'b', 'c', '\0' };
	memset(str1, 'i', 2);
	puts(str1); //iic

	char str2[] = "abc";
	memset(str2, 'u', sizeof(str2) - 1); //避免将最后的'\0'覆盖
	printf("%s", str2); //uuu
	return 0;
}

memcpy
  • 说明
void *memcpy(void *str1, const void *str2, size_t n)
/**
 * str1:指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
 * str2:指向要复制的数据源,类型强制转换为 void* 指针。
 * n:要被复制的字节数。
 * 和strcpy的区别在于,无论有多少个'\0',memcpy都会复制sizeof的n个长度
 **/
  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

int main(void)
{
	char str[] = "abc\0def";
	printf("sizeof(str) = %lu \n", sizeof(str)); //8
	char cpy1[10];
	char cpy2[10];
	strncpy(cpy1, str, sizeof(str)); //strcpy遇到\0将停止复制
	memcpy(cpy2, str, sizeof(str)); //memcpy不受\0的影响
	printf("strnpy: %s\n", cpy1 + strlen("abc") + 1); //
	printf("memcpy: %s\n", cpy2 + strlen("abc") + 1); //def
	
	int a[5] = { 1, 2, 3, 4, 5 };
	int b[5];
	memcpy(b, a, sizeof(a));
	return 0;

	//注意不要出现内存重叠
	//如 memcpy(&a[2], a, 5 * sizeof(int))
}
memmove
  • 说明

参见memcpy,用于补足内存重叠情形

  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main(void)
{
	int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	memmove(&a[2], a, 5 * sizeof(int));
	for (size_t i = 0; i < sizeof(a)/sizeof(a[0]); i++)
	{
		printf("a[%d] = %d\n", i, a[i]);
		/*
		a[0] = 1
		a[1] = 2
		a[2] = 1
		a[3] = 2
		a[4] = 3
		a[5] = 4
		a[6] = 5
		a[7] = 8
		a[8] = 9
		a[9] = 10
		*/
	}
	return 0;
}

memcmp
  • 说明

用于两个值的比较(对于数组从第一个元素逐个向后比较)

  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main(void)
{
	int a[] = { 1, 2, 3, 4, 5 };
	int b[] = { 1, 3, 2 };
	int res = memcmp(a, b);
	if (res > 0)
	{
		printf("a > b");
	}
	else if (res < 0)
	{
		printf("a < b");
	}
	else {
		printf("a == b");
	}
	//a < b
	return 0;
}
堆空间的使用
malloc & free
  • 说明
void* malloc(size_t size); //用于申请堆内存,size为指定的要申请的内存大小(字节数),返回值为申请到的内存地址,一般当做数组来使用
void free(void* ptr); //用于释放申请的堆内存,即释放malloc申请的内存,需要注意的是释放之后这部分内存并不会立即失效,所以往往还会进行赋NULL操作
  • 示例
#include<stdio.h>

int main(void)
{
	//int arr[100000] = { 0 }; //超出栈内存容量,直接无法启动,所以此时需要使用堆内存
	int* p = (int*)malloc(10 * sizeof(int)); //申请
    int* tmp = p; //用于free,防止p地址执行了p++等操作,导致地址变化
	for (size_t i = 0; i < 10; i++) //使用:赋值
	{
		p[i] = i + 10;
	}
	for (size_t i = 0; i < 10; i++) //使用:遍历
	{
		printf("p[%d] = %d\n", i, *(p+i));
	}
	free(tmp); //释放,注意不要再释放前对p进行++等操作,因为free要释放的必须是malloc申请的地址
    tmp = NULL; //立即失效
	return 0;
}
calloc & realloc
  • 说明
void* calloc(size_t nmemb, size_t size);
/*
 功能:在内存动态存储区中分配nmemb块长度为size字节的连续区域,并将分配到的内存置0值
 nmemb: 要分配的内存单元数量
 size: 每个内存单元的大小(单位为字节)
 返回值:成功返回分配空间的起始地址;失败返回NULL
 */

void* realloc(void* ptr, size_t size);
/*
 功能:对使用malloc/calloc函数在内存堆中分配到的空间进行重新分配
 扩大:如果指定的地址后面有连续的空间,那么就会在已有地址的基础上增加内存;如果没有则会重新分配新的连续空间,将就内存中的值拷贝到新内存,然后释放旧内存空间(新增空间不会置0)
 缩小:realloc会自动清理多余的旧的内存的数据
 ptr: 用malloc/calloc分配到的内存地址,如果此参数为NULL,那么realloc的功能将和malloc/calloc一致
 size: 为重新分配的堆内存的大小(单位字节)
 返回值:成功返回新分配到的内存的地址;失败返回NULL
 */
  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main(void)
{
	int* p = calloc(10, sizeof(int)); //相当于 malloc(sizeof(int) * 10),同时会将分配到的内存置0值
	for (size_t i = 0; i < 10; i++)
	{
		printf("p[%d]=%d ", i, p[i]); //p[0]=0 p[1]=0 p[2]=0 p[3]=0 p[4]=0 p[5]=0 p[6]=0 p[7]=0 p[8]=0 p[9]=0
	}
	if (p != NULL)
	{
		free(p);
		p = NULL;
	}
	return 0;
}

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main(void)
{
	int* p1 = calloc(10, sizeof(int));
	for (size_t i = 0; i < 10; i++)
	{
		printf("p[%d]=%d ", i, p1[i]); //p[0]=0 p[1]=0 p[2]=0 p[3]=0 p[4]=0 p[5]=0 p[6]=0 p[7]=0 p[8]=0 p[9]=0
	}

	//扩大:在原有地址的基础上
	int *p2 = realloc(p1, 11);
	printf("\np1=%p, p2=%p\n", p1, p2); //二者值一致
	printf("p[10]=%d", p2[10]); //随机数,即不会置0值

	//扩大:返回新的内存地址
	int* p3 = realloc(p2, 20);
	printf("p2=%p, p3=%p\n", p1, p2); //二者值不一致

	//缩小:只会在原有地址基础上
	int* p4 = realloc(p3, 5);
	printf("p[6]=%d", p3[6]); //随机值,即原有的多余空间会被自动释放

	if (p4 != NULL)
	{
		free(p4);
		p4 = NULL;
	}
	return 0;
}

二级指针malloc空间
#include<stdio.h>

int main(void)
{
	//1.申请外层空间
	int** p = malloc(sizeof(int*) * 3);
	//2.申请内存空间
	for (size_t i = 0; i < 3; i++)
	{
		p[i] = malloc(sizeof(int) * 5);
	}
	//3.写入数据
	for (size_t i = 0; i < 3; i++)
	{
		for (size_t j = 0; j < 5; j++)
		{
			p[i][j] = i + j;
		}
	}
	//4.读取数据
	for (size_t i = 0; i < 3; i++)
	{
		for (size_t j = 0; j < 5; j++)
		{
			printf("p[%d][%d] = %d\n", i, j, *(*(p + i) + j));
		}
		printf("\n");
	}
	//5.释放内层空间
	for (size_t i = 0; i < 3; i++)
	{
		free(p[i]);
		p[i] = NULL;
	}
	//6.释放外层空间
	free(p);
	p = NULL;
	return 0;
}

复杂数据类型

结构体
结构体说明
struct Student
{
    int age;
    char name[100];
    int score;
};
/*
1. struct是结构体关键字
2. struct Student合起来才是结构体类型(名称)
3. 需要封号结尾
4. 不能在结构体内部直接对定义的变量赋值
    【因为结构体只是一个类型,没有定义变量前是没有分配空间的】
 */
结构体示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

struct Student
{
	char name[100];
	int age;
	int score;
};
struct Student2
{
    char name[100];
	int age;
	int score;
} s1 = {"zhangsan", 18, 100}, s2; //可以同时定义(多个)变量,并进行初始化
struct //匿名结构体,只能使用此时定义的变量,不能再创建变量
{
	char name[100];
	int age;
	int score;
} s3, s4;

int main(void)
{
	/* 定义一个结构体类型变量:结构体名称 变量名称 */
	struct Student stu;

	/* 结构体变量初始化:只有在定义的时候才能初始化 */
	struct Student stu1 = { "张三", 18, 100 };

	/* 结构体变量的赋值:.点号(普通变量) / ->箭头(指针变量) */
	struct Student stu11;
	stu11.age = 18; //或:(&stu11) -> age = 19
	//stu11.name = "李四"; 数组是常量,不能直接赋值
	strcpy(stu11.name, "李四"); //或:strcpy((&stu11) -> name, "李四")
	stu11.score = 100; //或:(&stu11) -> score = 100

	struct Student *stu12;
	stu12 = &stu; //注意,如果是野指针,会报错
	stu12->age = 19; //或:(*stu12).age = 19
	strcpy(stu12->name, "王五"); //或:strcpy((*stu12).name, "王五")
	stu12->score = 100; //或:(*stu12).score = 100;

    /* 相同类型的结构体可以进行赋值 */
    struct Student stus999;
    stus999 = stu11; //同 int a = 1; int b; b = a;

	return 0;
}

结构体数组
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

struct Stu
{
	char name[100];
	int age;
};

int main(void)
{
	//定义时初始化
	struct Stu stus1[5] =
	{ //内部的花括号{}可以省略
		{"zhangsan", 1},
		{"lisi", 2},
		{"wangwu", 3},
		{"zhaoliu", 4},
		{"xiaoming", 5}
	};

	//多种赋值方式
	struct Stu stus2[5];
	strcpy(stus2[0].name, "zhangsan");
	stus2[0].age = 12;

	strcpy((stus2 + 1)->name, "lisi");
	(stus2 + 1)->age = 19;

	strcpy((*(stus2 + 2)).name, "wangwu");
	(*(stus2 + 2)).age = 20;

	struct Stu* p = stus2;
	strcpy((p + 3)->name, "zhaoliu");
	(p + 3)->age = 21;

	strcpy(p[4].name, "xiaoming");
	p[4].age = 21;

	for (size_t i = 0; i < sizeof(stus2)/sizeof(stus2[0]); i++)
    {
		printf("name=%s, age=%d \n", stus2[i].name, stus2[i].age);
	}
	return 0;
}
结构体嵌套结构体
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

struct BaseInfo
{
	char name[100];
	int age;
};

struct Player
{
	struct BaseInfo info;
	int score;
};

int main(void)
{
	struct Player player1;
	player1.info.age = 20;
	strcpy(player1.info.name, "zhangsan");
	player1.score = 100;

	struct Player* player2 = &player1;
	strcpy(player2->info.name, "lisi");
	player2->info.age = 21;
	player2->score = 90;

	struct Player player3 = { "wangwu", 22, 91 }; //会自动按顺序赋值

	printf("%s, %d, %d", player2->info.name, player2->info.age, player2->score);
	return 0;
}
结构体指针成员
  • 问题
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

struct A
{
	char* name;
};

int main(void)
{
	struct A a;
	strcpy(a.name, "abc"); //error:name是个野指针,无法接受拷贝数据
	return 0;
}
  • 方案一:指向文字常量区(data区)
int main(void)
{
	struct A a;
	a.name = "abc"; //字符串实质就是其内存首地址,所以相当于:&变量
	return 0;
}
  • 方案二:指向栈区内存
int main(void)
{
	struct A a;
	char temp[100];
	a.name = temp;
	strcpy(a.name, "abc");
	puts(temp); //abc
	return 0;
}
  • 方案三:指向堆区内存
#include<stdlib.h>
int main(void)
{
	struct A a;
	a.name = (char*)malloc((strlen("abc") + 1) * sizeof(char));
	if (a.name != NULL)
	{
		strcpy(a.name, "abc");
		puts(a.name); //abc
	}
    if (a.name != NULL)
    {
        free(a.name);
        a.name = NULL;
    }
	return 0;
}
  • 指针结构体嵌套指针成员
int main(void)
{
	struct A *a;
	//分配内存
	a = (struct A*)malloc(sizeof(struct A));
	a->name = (char*)malloc((strlen("abc") + 1) * sizeof(char));
	//赋值
	strcpy((*a).name, "abc");
	//打印
	printf("name=%s\n", a->name);
	//释放:先内部
	if (a->name != NULL)
	{
		free(a->name);
		a->name = NULL;
	}
	//释放:再外部
	if (a != NULL)
	{
		free(a);
		a = NULL;
	}
	return 0;
}
结构体嵌套二级指针
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

typedef struct Teacher
{
	char* name;
	char** students;
} Teacher;

void allocateSpace(Teacher*** teachers)
{
	if (NULL == teachers) return;
	//1.分配老师数组
	Teacher** tArray = malloc(sizeof(Teacher*) * 3);
	//2.分配老师
	for (size_t i = 0; i < 3; i++)
	{
		tArray[i] = malloc(sizeof(Teacher));
		//3.分配老师姓名并赋值
		tArray[i]->name = malloc(sizeof(char) * 64);
		sprintf(tArray[i]->name, "Teacher_%d", i + 1);
		//4.分配学生数组
		tArray[i]->students = malloc(sizeof(char*) * 5);
		for (size_t j = 0; j < 5; j++)
		{
			//5.分配学生名字并赋值
			(tArray[i]->students)[j] = malloc(sizeof(char) * 64);
			sprintf((tArray[i]->students)[j], "%s_Student_%d", tArray[i]->name, j + 1);
		}
	}
	*teachers = tArray;
}

void showSpace(Teacher** teachers)
{
	if (NULL == teachers) return;
	for (size_t i = 0; i < 3; i++)
	{
		puts(teachers[i]->name);
		for (size_t j = 0; j < 5; j++)
		{
			printf("     %s\n", teachers[i]->students[j]);
		}
	}
}

void freeSpace(Teacher** teachers)
{
	if (NULL == teachers) return;
	for (size_t i = 0; i < 3; i++)
	{
		//1.释放老师名字
		if (NULL != teachers[i]->name)
		{
			free(teachers[i]->name);
			teachers[i]->name = NULL;
		}
		for (size_t j = 0; j < 5; j++)
		{
			//2.释放学生姓名
			if (NULL != teachers[i]->students[j])
			{
				free(teachers[i]->students[j]);
				teachers[i]->students[j] = NULL;
			}
		}
		//3.释放学生数组
		if (NULL != teachers[i]->students)
		{
			free(teachers[i]->students);
			teachers[i]->students = NULL;
		}
		//4.释放老师
		if (NULL != teachers[i])
		{
			free(teachers[i]);
			teachers[i] = NULL;
		}
	}
	//5.释放老师数组
	if (NULL != teachers)
	{
		free(teachers);
		teachers = NULL;
	}
}

int main(void)
{
	Teacher** teachers = NULL;

	//分配内存
	allocateSpace(&teachers);

	//展示数据
	showSpace(teachers);

	//释放内存
	freeSpace(teachers);

	return 0;
}
结构体拷贝
  • 系统默认的浅拷贝
#include<stdio.h>
#include<string.h>

typedef struct P
{
	char name[64];
	int age;
} P;

int main(void)
{
	P p1 = { "zhangsan", 18 };
	P p2 = { "lisi", 20 };
	/*
	系统使用的是浅拷贝,即将p1指向的地址 指向 p2指向的地址
	*/
	p1 = p2;
	printf("p1: name=%s, age=%d", p1.name, p1.age);
	return 0;
}
  • 系统浅拷贝所引起的问题
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

typedef struct P
{
	char* name;
	int age;
} P;

int main(void)
{
	P p1;
	P p2;

	p1.name = malloc(sizeof(char) * 64);
	strcpy(p1.name, "zhangsan");
	p1.age = 18;
	p2.name = malloc(sizeof(char) * 128);
	strcpy(p2.name, "lisi");
	p2.age = 20;
	
	p1 = p2; //系统浅拷贝
	printf("p1: name=%s, age=%d", p1.name, p1.age);

	if (p1.name != NULL)
	{
		free(p1.name);
		p1.name = NULL;
	}

	if (p2.name != NULL)
	{
		/*
		问题一:运行时,此处报错,原因是重复释放内存
		问题二:原本的p1.name = "zhangsan"无处释放,即存在内存泄露问题
		*/
		free(p2.name);
		p2.name = NULL;
	}

	return 0;
}

  • 手动深拷贝的解决方案
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>

typedef struct P
{
	char* name;
	int age;
} P;

int main(void)
{
	P p1;
	P p2;

	p1.name = malloc(sizeof(char) * 64);
	strcpy(p1.name, "zhangsan");
	p1.age = 18;
	p2.name = malloc(sizeof(char) * 128);
	strcpy(p2.name, "lisi");
	p2.age = 20;
	
	/*采用手动深拷贝方式*/
	//1.先释放原有空间(由于p2.name与p1.name开辟的空间不尽相同,所以最后先释放)
	if (p1.name != NULL)
	{
		free(p1.name);
		p1.name = NULL;
	}
	//2.重新申请空间
	p1.name = malloc(strlen(p2.name) + 1);
	//3.赋值
	strcpy(p1.name, p2.name);
	p1.age = p2.age;

	printf("p1: name=%s, age=%d", p1.name, p1.age);

	if (p1.name != NULL)
	{
		free(p1.name);
		p1.name = NULL;
	}

	if (p2.name != NULL)
	{
		/*
		运行时,此处报错,原因是重复释放内存
		此外原本的p1.name = "zhangsan"无处释放,即存在内存泄露问题
		*/
		free(p2.name);
		p2.name = NULL;
	}
	return 0;
}

结构体字节对齐
  • 字节对齐使得实际占用字节数往往会偏大
#include<stdio.h>

struct Demo1
{
	int a; 
	char b;
};

int main(void)
{
	printf("Demo1: %lu", sizeof(struct Demo1)); //8 【理论上是:4(int) + 1(char) = 5】
	return 0;
}
  • 字节对齐的原因
  1. 平台原因(移植原因)

不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

  1. 性能原因(空间换时间)

数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问
在这里插入图片描述
如上图,在32位操作系统中,数据总线是32位,所以一次读写4字节,如果未对齐,则对于其中的i(int),需要读取两次才能确定数值,而对齐后只需一次

  • 对齐规则
  1. 第一个成员偏移量为0
  2. 其余成员偏移量为对齐数整数倍
  3. 对齐数 = min ("成员中字节数最大者字节数", #pragma pack(指定的对齐数))
  4. 如果有嵌套结构体,对齐数取内外层结构体中的较大者

  • 示例一
#include<stdio.h>

struct Demo1
{
	short a;
	char b;
};

int main(void)
{
	/*
	对齐数 = 2
	a: 2 * 0 = 0
	b: 1 * 1 = 1  //自身字节数整数倍,直至对应偏移量位置上可以存储为止
    b: 1 * 2 = 2 //可以

	a a   (0 ~ 1)
	b *   (2 ~ 3)

	*/
	printf("Demo1: %lu", sizeof(struct Demo1)); //4
	return 0;
}
  • 示例二
#include<stdio.h>

struct Demo2
{
	short a;
	double b;
	char c[2];
};

int main(void)
{
	/*
	对齐数 = 8(double)
	a: 2 * 0 = 0
	b: 8 * 1 = 8 //该位置上没有存值,可以
	c: 2 * 1 = 1
	c: ...
	c: 2 * 8 = 16

	a a * * * * * *  (0 ~ 7)
	b b b b b b b b  (8 ~ 15)
	c * * * * * * *  (16 ~ 23)

	*/
	printf("Demo2: %lu", sizeof(struct Demo2)); //24
	return 0;
}
  • 示例三:嵌套结构体
#include<stdio.h>

int main(void)
{
	typedef struct Demo1
	{
		double a1;
		short b1;
	} Demo1;

	struct
	{
		char a2;
		Demo1 b2;
		int c2;
	} Demo2;
	/*
	对齐数 = 8(double)
	a2: 2 * 0 = 0
	b2: (不可接在外层结构体成员中)
		a1: 8 * 1 = 8
		b1: 2 * 8 = 16
	c2: 4 * 6 = 24 (不可接在结构体b2中)

	a2  *  *  *  *  *  *  *  (0 ~ 7)
	a1 a1 a1 a1 a1 a1 a1 a1  (8 ~ 15)
	b1 b1  *  *  *  *  *  *  (16 ~ 23)
	c2 c2 c2 c2  *  *  *  *  (24 ~ 32)

	*/
	printf("Demo2: %lu", sizeof(Demo2)); //32
	return 0;
}
  • 设置对齐数
  1. 对齐数 > 最大成员字节数

取最大成员字节数为实质对齐数

#include<stdio.h>
#pragma pack(8)

int main(void)
{
	struct
	{
		char a;
		int b;
	} Demo;
	printf("Demo: %lu", sizeof(Demo)); //8
	return 0;
}
  1. 对齐数 < 最大成员字节数

字节数超过对齐数的成员计算偏移量时以对齐数进行计算

#include<stdio.h>
#pragma pack(2) //设置对齐数为2

int main(void)
{
	struct
	{
		char a;
		int b;
	} Demo;
	// 对齐数 = 2
	// a: 1 * 0 = 0
	// b: 2 * 1 = 2
	//
	// a * 
	// b b
	// b b
	printf("Demo: %lu", sizeof(Demo)); //6
	return 0;
}
结构体位段
  • 说明

结构体中允许存在位段、无名字段以及字对齐所需的填充字段。这些都是通过在字段的声明后面加一个冒号以及一个表示字段位长的整数来实现。这些冒号后的整数规定了成员所占的位数

  • 示例
#include<stdio.h>

int main(void)
{
	struct
	{
		char a : 4;
		int b : 8;
	} Demo;
	Demo.a = 0xf0; //会截断,只取前4位
	Demo.b = 257; //会截断,只取前8位,即一字节
	
	/*1 2 4 8 16 32 64 128
	0xf0: 240 11110000,前4位:0
	257: 1 00000001,前8位:1
	*/
	printf("a = %d\n", Demo.a); //0
	printf("b = %d\n", Demo.b); //1
	return 0;
}
  • 结构体存储压缩

如果结构体中相连且相同类型的成员指定了位段,且位段总和小于其原始类型字节数,则会视情况将这些成员压缩到一个字节类型空间中

#include<stdio.h>

int main(void)
{
	struct
	{
		int a : 4;
		int b : 8;
	} Demo;
	
	printf("Demo: %lu", sizeof(Demo)); //4,压缩到了一个int中
	return 0;
}
共用体(联合体)
  • 说明
  1. 所有成员公用相同的内存地址
  2. 大小为成员最大的字节数
  3. 由于公用内存,所以改动一个成员,其余成员将受影响
  • 示例
#include<stdio.h>

int main(void)
{
	union
	{
		unsigned char a;
		unsigned int b;
		unsigned short c;
	} Demo;
	printf("大小为成员中字节数最大的:%lu\n", sizeof(Demo)); //4
	Demo.b = 0x11223344; //首地址为低位,从低位开始赋值
	printf("a=%x\n", Demo.a); //44
	printf("b=%x\n", Demo.b); //11223344
	printf("c=%x\n", Demo.c); //3344
	Demo.a = 0xff;
	printf("a=%x\n", Demo.a); //ff
	printf("b=%x\n", Demo.b); //112233ff
	printf("c=%x\n", Demo.c); //33ff
	return 0;
}
枚举
#include<stdio.h>

enum Color
{
	GREEN,
	YELLOW,
	RED=10,
	BLUE,
	PINK
};

int main(void)
{
	/*给枚举变量赋值*/
	enum Color flag = GREEN;
	enum Color flag2 = 1; //相当于YELLOW,一般不会这么写

	/*如果未赋值,则默认首个枚举常量值为0,后续递增1*/
	printf("%d, %d, %d, %d, %d", GREEN, YELLOW, RED, BLUE, PINK); //0, 1, 10, 11, 12
	return 0;
}

文件处理

概述
typedef struct
{
	short level; //缓冲区状态(满/空的程度)
	unsigned flags; //文件状态标志
	char fd; //文件描述符
	unsigned char hold; //如无缓冲区不读取字符
	short bsize; //缓冲区大小
	unsigned char * buffer; //数据缓冲区的位置
	unsigned ar; //指针,指向当前的位置
	unsigned istemp; //临时文件,指示器
	short token; //用于有效性的检查
} FILE;

FILE * fp;
  1. 对于各平台,结构体FILE内部的成员变量不尽一致,但是结构体变量名称均为FILE
  2. 只要fp调用了fopen()函数,该函数就会在堆区申请空间并将地址返回给fp
  3. 并非是fp关联文件,而是内部成员保存了文件的相关信息
  4. 不要直接操作fp指针,而是通过相关函数,函数的调用会自动调整fp中成员的相关值
  5. 文件描述符,每打开一个文件对应就给这个文件一个int值,用于标识,在linux中可以使用ulimit -a命令,其中open files就是描述符取值范围,其中0(stdin), 1(stdout), 2(stderr)三个值默认为系统占用
三大特殊文件指针

C语言中有三个特殊的文件指针由系统默认打开,无需用户定义即可直接使用

  1. stdin

标准输入,默认为当前终端(键盘),scanf, getchar等函数默认从此终端获得数据

  1. stdout

标准输出,默认为当前终端(屏幕),printf, puts等函数默认输出数据到此终端

  1. stderr

标准出错,默认为当前终端(屏幕),perror函数默认输出数据到此终端

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main(void)
{
	int a;
	printf("请输入:");
	scanf("%d", &a);
	printf("a = %d\n", a);
	fclose(stdin); //关闭标准输入
	scanf("%d", &a); //不会 阻塞并让用户输入
	perror("stdin error"); //输出报错原因(只能用于库函数)

	fclose(stdout); //关闭标准输出
	printf("========="); //不会输出
	perror("stdout error: ");

	fclose(stderr); //关闭标准出错
	perror("stderr error: "); //不会输出

	return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>

int main(void)
{
	printf("aaaaaaa");
	_close(1); //1对应标准输出设备
	int fd = _open("test.txt", O_WRONLY, 0777);
	printf("fd = %d\n", fd); //1,说明此时将1分配给了test.txt,而不再是标准输出
	printf("bbbbbbb"); //将写入到test.txt
	return 0;
}

VS环境下的相对路径
  1. 如果是编译运行,则相对位置起点是项目名称.vcxproj所在路径;
  2. 如果是直接双击xxx.exe,则是xxx.exe所在路径
常用函数
fopen & fclose
  • 函数声明
FILE * fopen(const char* filename, const char * mode);
/*
filename: 文件路径 + 文件名
mode: 文件打开方式
   r/rb: 只读,不会自动创建文件
   w/wb: 写,文件存在则清空文件,不存在则自动创建
   a/ab: 追加,文件存在则追加内容,不存在则自动创建
   r+/rb+: 可读写,不会自动创建文件
   w+/wb+: 可读写, 文件存在则清空文件,不存在则自动创建
   a+/ab+: 可读写, 文件存在则追加内容,不存在则自动创建
   注意1:b是二进制的意思,只在Windows有效
   注意2:在Windows平台下,以文本方式打开文件,不加b,则
       1. 当读取文件时,系统会将所有\r\n转换成\n
       2. 当写入文件时,系统会将\n转换成\r\n
       3. 以二进制方式打开文件,则不会有以上的转换
   注意3:在Linux平台下,“文本”与“二进制”模式没有区别,"\r\n" 作为两个字符原样输入输出
返回值: 文件指针,若失败则返回NULL
 */

int fclose(FILE * strean);
//返回值:成功0,失败-1
  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

int main(void)
{
	FILE* fp = fopen("test.txt", "r");
	if (NULL == fp)
	{
		perror("fopen error");
		return -1;
	}
	fclose(fp);
	return 0;
}
fgetc & fputc
  • 声明
int fputc(int ch, FILE * stream);
/*
ch: 待写入的字符
返回值:如果写入成功,返回写入字符的ASCII码;失败则返回 -1
 */

int fgetc(FILE * stream);
//返回值:成功返回读到的字符的ASCII码;失败返回 -1
  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

int main(void)
{
	FILE* fp = fopen("test.txt", "w");
	if (NULL == fp)
	{
		perror("fopen error");
		return -1;
	}
	int ret = fputc('A', fp);
	printf("ret = %d", ret); //65
	fclose(fp);
	return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

int main(void)
{
	FILE* fp = fopen("test.txt", "r");
	if (NULL == fp)
	{
		perror("fopen error");
		return -1;
	}
	while (1)
	{
		int ch = fgetc(fp);
		if (EOF == ch) //文件读取结束标记为 EOF,值为 - 1
		{
			printf("end ------- ");
			break;
		}
		printf("ch = %c\n", ch);
	}
	fclose(fp);
	return 0;
}
fgets & fputs
  • 声明
int fputs(const char* str, FILE * stream);
/*
 成功返回0,失败返回-1
 */

char * fgets(char* s, int size, FILE * stream);
/*
 s: 接受读取结果的字符串
 size: 指定最大读取字符串的长度(size -1:用于存放'\0')
 返回值:成功则返回读取到的字符串;读取到文件尾/失败时则返回NULL
 注意1:当遇到换行符,或到文件尾,或读取size-1长度时。最后自动添加'\0'
 注意2:当空间充足时会读取\n换行符,如 s = char buf[10]; 如果文件内容为 hello,则会保存的值为 hello\n\0
 */
  • 示例
int main(void)
{
	char buf[10] = { 0 };
	fgets(buf, 10, stdin);
	printf("buf: %s", buf);
    FILE* fp = fopen("test.txt", "w");
	fputs(buf, fp);
	fclose(fp);
	return 0;
}
fprintf & fscanf
  • 声明
int fprintf(FILE * stream, const char * format, ...);
/*
 功能:根据format来转换并格式化数据,然后将结果输出到stream对应的文件中,知道出现\0为止
 format: 字符串格式,用法和printf()一样
 返回值:成功返回实际写入文件的字符个数;失败返回-1
 */

int fscanf(FILE* stream, const char * format, ...);
/*
 1. 从stream对应的文件中读取数据,并根据format来转换并格式化数据
 2. 成功返回转换成功的值的个数,失败返回-1
 3. 一次读取一行
 4. 存在边界溢出问题,即只有三行却读了4次,如果都用相同的变量接受的话,最后一次展示的是前一次的结果,所以在使用前最后将变量清空
 5. 每次在调用时都会判断下一次调用是否匹配format,如果不匹配,提前结束(使得feof为真),这样会导致最后一行被跳过
 */
  • 示例
fprintf(fp, "%d = %d %c %d\n", 15, 3, '*', 5);
int main(void)
{
	FILE* fp = fopen("test.txt", "r");
	int sum, a, b;
	char operate;
	int count = fscanf(fp, "%d=%d%c%d", &sum, &a, &operate, &b);
	fclose(fp);
	printf("count=%d\n", count); //count=4
	printf("%d=%d%c%d", sum, a, operate, b); //10=1+9
	return 0;
}
fread & fwrite
  • 声明
/* 主要用于处理二进制文件 */

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE * stream);
/*
作用:以数据块的方式给文件写入内容
 ptr: 准备写入文件的数据的地址
 size: 指定写入文件内容的块数据大小(字节数)
 nmemb: 写入文件的块数,写入文件数据总大小为(size * nmemb)
 返回值:nmemb值(成功返回实际写入文件数据的块数目,不过如果块数大于实际数据数,也是返回nmemb值大小);失败返回0
 */

size_t fread(void * ptr, size_t size, size_t nmemb, FILE * stream);
/*
 功能:以数据块方式从文件中读取内容
 ptr: 用于接受数据的内存地址
 返回值:成功返回实际读的文件数据的块数目(即nmemb值,不过如果nmemb比数据块数目大,返回的是实际块数);失败或读到文件尾则返回0
 */
  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

typedef struct Demo
{
	int age;
	char name[10];
	int number;
} Demo;

int main(void)
{
	printf("sizeof(Demo) = %lu\n", sizeof(Demo)); //20
	Demo arr[3] = {
		{18, "zhangsan", 9},
		{21, "lisi", 12},
		{20, "wangwu", 10}
	};
	FILE* fp = fopen("test.txt", "w");
	if (!fp)
	{
		perror("fopen error");
		return -1;
	}
	int bytes = fwrite(&arr[0], 1, sizeof(Demo) * 3, fp);
	printf("bytes=%d\n", bytes); //60
	fclose(fp);
	return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>

typedef struct Demo
{
	int age;
	char name[10];
	int number;
} Demo;

int main(void)
{

	FILE* fp = fopen("test.txt", "r");
	if (!fp)
	{
		perror("fopen error");
		return -1;
	}
	Demo arr[3];
	Demo* p = arr;
	while (1)
	{
		int ret = fread(p++, 1, sizeof(Demo), fp);
		printf("ret=%d\n", ret);
		if (ret == 0) // 或 feof(fp)
		{
			break;
		}
	}
	fclose(fp);
	Demo temp;
	for (size_t i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
	{
		temp = *(arr + i);
		printf("%d: name=%s, age=%d, num=%d\n", i, temp.name, temp.age, temp.number);
	}
	return 0;
	/*
	ret=20
	ret=20
	ret=20
	ret=0
	0: name=zhangsan, age=18, num=9
	1: name=lisi, age=21, num=12
	2: name=wangwu, age=20, num=10
	*/
}

  • 文件复制
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void mycopy()
{
	FILE* rfile = fopen("C:\\Users\\ysw15\\Pictures\\Saved Pictures\\帅.jpg", "rb");
	FILE* wfile = fopen("C:\\Users\\ysw15\\Pictures\\Saved Pictures\\帅2.jpg", "wb");

	int ret;
	char buf[1024] = { 0 }; //缓冲区,设置每次读取的大小
	while (1)
	{
		ret = fread(buf, 1, sizeof(buf), rfile);
		if (0 == ret)
		{
			break;
		}
		fwrite(buf, 1, ret, wfile);
	}

	fclose(rfile);
	fclose(wfile);
}

int main(void)
{
	mycopy();
	return 0;
}
remove & rename
  • 声明
int remove(const char * pathname);
/*
 功能:删除文件
 返回值:成功0;失败-1
 */

int rename(const char* oldpath, const char* newpath);
/*
 功能:将oldpath名称改为newpath
 返回值:成功0;失败-1
 */
fseek & ftell & rewind
  • 声明
/*每次文件读写中,都对应有一个文件读写指针(读指针和写指针是同一个)存在,指示着当前读写的位置*/

int fseek(FILE * stream, long, offset, int whence);
/*
 功能:从stream的指定读写位置处开始读
 offset: 根据whence来移动的位移数(偏移量),可以是正负数,正数相对于wnence右移(如果移动的字节数超过了文件尾,则再次写入时将增大文件尺寸),负数则左移动(如果左移的字节数超过了文件开头则出错返回)
 whence: 
    1. SEEK_SET: 从文件开头移动offset个字节
    2. SEEK_CUR: 从当前位置移动offset个字节
    3. SEEK_END: 从文件末尾移动offset个字节
 返回值:成功0,失败-1
 */

long ftell(FILE * stream);
/*
 功能:获取文件流的读写位置(当前读写位置到开头的偏移量)
 返回值:成功返回当前文件流的读写位置;失败返回-1
 常用:和fseek(fp, 0, SEEK_END)结合以获取文件大小
 */

void rewind(FILE * stream);
/*
 功能:将文件流的读写位置移动到文件开头(回卷文件读写指针)
 */

  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct Student
{
	int age;
	char name[10];
	int number;
} Student;

int main(void)
{
	Student stus[4] =
	{
		{18, "zhangsan", 201},
		{19, "lisi", 302},
		{20, "wangwu", 109},
		{21, "zhaoliu", 209}
	};

	FILE * fp = fopen("C:\\Users\\ysw15\\Pictures\\Saved Pictures\\test", "wb+");

	//写入文件
	fwrite(&stus[0], 1, sizeof(stus), fp);

	//读取wangwu数据
	fseek(fp, sizeof(Student) * 2, SEEK_SET);
	Student wangwu;
	fread(&wangwu, 1, sizeof(Student), fp);
	printf("1. name=%s, age=%d, number=%d\n", wangwu.name, wangwu.age, wangwu.number); //1. name=wangwu, age=20, number=109

	//获取当前读写指针偏移量
	int offset = ftell(fp);
	printf("当前偏移量为:%d\n", offset); //60

	//回到文件起始位置
	rewind(fp);

	//读取zhangsan数据
	Student zhangsan;
	fread(&zhangsan, 1, sizeof(Student), fp);
	printf("2. name=%s, age=%d, number=%d\n", zhangsan.name, zhangsan.age, zhangsan.number); //2. name=zhangsan, age=18, number=201

	//获取文件大小
	fseek(fp, 0, SEEK_END); //将读写指针放到未见末尾
	int size = ftell(fp); //读取文件偏移量大小
	printf("文件的大小为:%d\n", size); //80

	fclose(fp);
	return 0;
}
stat
  • 声明
#include<sys/types.h>
#include<sys/stat.h>

int stat(const char* path, struct stat* buf);
/*
 功能:获取文件状态信息
 path: 文件
 buf: 用于接收文件信息的结构体
 返回值:成功0;失败-1
 */

struct stat 
{
    def_t st_dev; //文件的设备编号
    ino_t st_ino; //节点
    mode_t st_mode; //文件的类型和存取的权限
    nlink_t st_nlink; //连到该文件的硬连接数据,刚建立的文件该值为1
    uid_t st_uid; //所属用户ID
    gid_t st_gid; //所属组ID
    dev_t st_rdev; //设备类型,如果该文件尾设备文件,则此值为其设备编码
    off_t st_size; //文件字节数(文件大小)
    unsigned long st_blksize; //块大小(文件系统的I/O缓冲区大小)
    unsigned long st_blocks; 块数
    time_t st_atime; //最后一次访问时间
    time_t st_mtime; //最后一次修改时间
    time_t st_ctime; //最后一次(属性)变更时间
}
  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>

int main(void)
{
    struct stat buf;
    int ret = stat("test.txt", & buf);
    printf("file size is %d\n", buf.st_size);
    return 0;
}

fflush
  • 声明
int fflush(FILE * stream);
/*
 功能:用于手动更新缓冲区,使得缓冲区数据立刻输出到对应的物理设备上(fclose会自动刷新缓冲区)
 返回值:成功0;失败-1
 */
feof
  • 声明
int feof(FILE * stream);
/*
 1. 文件读取结束标记为 EOF,值为 -1
 2. feof函数用来判断是否到达文件底部,弥补了文件中存在值为-1的数据,使用EOF导致提前结束读取的问题
 3. 当没有达到文件结尾时返回0,否则返回非0
 4. 必须有读取文件的动作,否则FILE*内部变量状态未变,即一直都是未到底部的状态(空文件除外)。
 */
  • 示例
if (feof(fp))
{
    fgetc(fp);
    break;
}
WIndows和Linux的先读后写差异
  • 说明

对于先读后写的情况,在进行写入时,Windows下返回写入成功,但是实质上并未真正写入,此时需要添加额外的语句fseek(fp, 0, SEEK_CUR);

  • 示例
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

int main(void)
{
	/*原始内容:111112222233333*/
	FILE* fp = fopen("C:\\Users\\ysw15\\Pictures\\Saved Pictures\\test.txt", "rb+");

	char buf[6] = { 0 };
	char* ptr = fgets(buf, 6, fp);
	printf("buf=%s, ptr=%s\n", buf, ptr); //buf=11111, ptr=11111

	/*
	在Linux下,这种先读后写的情况,由于读写指针为同一指针,源文件会被直接改为11111AAAAA33333;
	在Windows下,还需要一个看似无用的额外语句
	*/
	fseek(fp, 0, SEEK_CUR); //Windows额外语句
	int ret = fputs("AAAAA", fp);
	printf("ret = %d", ret); //0

	fclose(fp);
	return 0;
}

链表

使用数组的缺陷
  1. 是个静态空间,即一旦分配内存就不再可以动态扩展,所以一旦分配过多则造成资源浪费
  2. 对于删除与插入的执行效率低下
链表的分类

一、根据内存分配区域

  1. 静态链表:在栈区分配
  2. 动态链表:在堆区分配
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

typedef struct LinkNode
{
	int value;
	struct LinkNode* nextNode;
} LinkNode;

staticLinkDemo()
{
	/*创建节点*/
	LinkNode node1 = { 100, NULL };
	LinkNode node2 = { 200, NULL };
	LinkNode node3 = { 300, NULL };
	LinkNode node4 = { 400, NULL };
	LinkNode node5 = { 500, NULL };

	/*建立关系*/
	node1.nextNode = &node2;
	node2.nextNode = &node3;
	node3.nextNode = &node4;
	node4.nextNode = &node5;

	/*打印数据*/
	LinkNode* temp = &node1;
	while (1)
	{
		printf("%d\t", temp->value); //100     200     300     400     500
		if (!temp->nextNode) break;
		else temp = temp->nextNode;
	}
	printf("\n");
}

dynamicLinkDemo()
{
	/*分配内存*/
	LinkNode *node1 = malloc(sizeof(LinkNode));
	LinkNode *node2 = malloc(sizeof(LinkNode));
	LinkNode *node3 = malloc(sizeof(LinkNode));
	LinkNode *node4 = malloc(sizeof(LinkNode));
	LinkNode *node5 = malloc(sizeof(LinkNode));

	/*设置数据域*/
	node1->value = 100;
	node2->value = 200;
	node3->value = 300;
	node4->value = 400;
	node5->value = 500;

	/*设置指针域*/
	node1->nextNode = node2;
	node2->nextNode = node3;
	node3->nextNode = node4;
	node4->nextNode = node5;
	node5->nextNode = NULL;

	/*打印数据*/
	LinkNode* temp = node1;
	while (1)
	{
		printf("%d\t", temp->value); //100     200     300     400     500
		if (!temp->nextNode) break;
		else temp = temp->nextNode;
	}

    /*释放空间*/
}

int main(void) {
	/*静态链表*/
	staticLinkDemo();

	/*动态链表*/
	dynamicLinkDemo();
	return 0;
}


二、根据链表结构特点

  1. 单向链表
  2. 双向链表
  3. 单向循环链表
  4. 双向循环链表

三、根据是否有固定的头节点

  1. 带头链表
    有一个固定节点作为头节点,该节点只作为一个标志位的作用,数据域不保存有效数据
  2. 不带头链表
    指的是头结点不固定,根据实际需要变换头结点,如在原来头节点前插入新节点,则新节点将成为新的头结点
链表的实现
  • myList.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

typedef struct LinkNode
{
	int value; //数据域
	struct LinkNode* nextNode; //指针域
} LinkNode;

/*初始化*/
LinkNode* initList();

/*遍历*/
void foreachNode(LinkNode* header);

/*在指定的beforeValue前面插入value,如果beforeValue不存在,则相当于尾插*/
LinkNode* insertNode(LinkNode* header, int beforeValue, int value);

/*删除节点*/
int deleteNode(LinkNode* header, int value);

/*清空链表*/
int clearList(LinkNode* header);

/*销毁链表*/
int destroyList(LinkNode* header);

/*反转链表*/
void reverseList(LinkNode* header);

/*节点个数*/
int sizeOfList(LinkNode* header);

  • myList.c
#include "myList.h"

//初始化
LinkNode* initList()
{
	LinkNode* header = malloc(sizeof(LinkNode));
	if (!header)
	{
		perror("初始化失败");
		return NULL;
	}
	//初始化指针域:保证不为野指针
	header->nextNode = NULL;
	return header;
}

//遍历
void foreachNode(LinkNode* header)
{
	if (!header) return;
	LinkNode* currentNode = header->nextNode;
	while (NULL != currentNode)
	{
		printf("%d\n", currentNode->value);
		currentNode = currentNode->nextNode;
	}
}


//插入
LinkNode* insertNode(LinkNode* header, int beforeValue, int value)
{
	if (!header) return;
	LinkNode* previousNode = header;
	LinkNode* currentNode = header->nextNode;
	while (NULL != currentNode)
	{
		if (beforeValue == currentNode->value) break;
		previousNode = currentNode;
		currentNode = currentNode->nextNode;
	}
	//创建节点
	LinkNode* newNode = calloc(1, sizeof(LinkNode));
	if (!newNode)
	{
		perror("插入失败");
		return NULL;
	}
	//赋值
	newNode->value = value;
	//建立关系
	newNode->nextNode = currentNode;
	previousNode->nextNode = newNode;
	return newNode;
}

//删除节点
int deleteNode(LinkNode* header, int value)
{
	if (!header) return EXIT_FAILURE;
	LinkNode* previousNode = header;
	LinkNode* currentNode = header->nextNode;
	while (NULL != currentNode)
	{
		if (value == currentNode->value) break;
		previousNode = currentNode;
		currentNode = currentNode->nextNode;
	}
	
	if (NULL != currentNode)
	{
		//建立关系
		previousNode->nextNode = currentNode->nextNode;
		//释放内存
		free(currentNode);
		currentNode = NULL;
		return EXIT_SUCCESS;
	}
	else {
		perror("节点不存在");
		return EXIT_FAILURE;
	}
}

//清空链表
int clearList(LinkNode* header)
{
	if (!header) return EXIT_FAILURE;
	LinkNode* currentNode = header->nextNode;
	LinkNode* nextNode;
	while (NULL != currentNode)
	{
		nextNode = currentNode->nextNode;
		free(currentNode);
		currentNode = nextNode;
	}
	header->nextNode = NULL;
	return EXIT_SUCCESS;	
}

//销毁链表
int destroyList(LinkNode* header)
{
	if (!header) return EXIT_FAILURE;
	if (EXIT_SUCCESS == clearList(header))
	{
		free(header);
		header = NULL;
		return EXIT_SUCCESS;
	}
	return EXIT_FAILURE;
}


/*反转链表*/
void reverseList(LinkNode* header)
{
	if (!header) return;
	LinkNode* previousNode = NULL;
	LinkNode* currentNode = header->nextNode;
	LinkNode* nextNode = NULL;
	while (NULL != currentNode)
	{
		nextNode = currentNode->nextNode;
		currentNode->nextNode = previousNode;
		previousNode = currentNode;
		currentNode = nextNode;
	}
	header->nextNode = previousNode;
}


/*节点个数*/
int sizeOfList(LinkNode* header)
{
	if (!header) return -1;
	LinkNode* currentNode = header->nextNode;
	int size = 0;
	while (NULL != currentNode)
	{
		size++;
		currentNode = currentNode->nextNode;
	}
	return size;
}

  • main.c
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include "myList.h"

int main(void)
{
	LinkNode* header = initList();
	insertNode(header, -1, 100);
	insertNode(header, -1, 80);
	insertNode(header, -1, 70);
	insertNode(header, -1, 60);
	insertNode(header, 80, 90);
	foreachNode(header); //100 90 80 70 60
	deleteNode(header, 100);
	printf("\n");
	foreachNode(header); //90 80 70 60
	clearList(header);
	insertNode(header, -1, 100);
	printf("\n");
	foreachNode(header); //100
	insertNode(header, -1, 90);
	insertNode(header, -1, 80);
	reverseList(header);
	printf("\n");
	foreachNode(header); //80 90 100
	printf("\n%d", sizeOfList(header)); //3
	destroyList(header);
	return EXIT_SUCCESS;
}

几个函数与关键字

putchar
  • 说明

输出一个字符到屏幕上

  • 示例
putchar(97);
putchar('a');
puts
  • 说明

将一个字符串(只能是字符串)输出到屏幕上,自带换行符

scanf
  • 说明

接收键盘输入的数据

  • scanf_s

建议使用 scanf_s,因为scanf在读取时不会检查边界,存在造成内存访问越界的问题,如分配了5字节的空间,但是读入了10字节,多余的部分会被写入到别的空间上去,

//注意,在ANSI C中没有scanf_s,只有scanf
char buf[6] = {'\0'};
//scanf("%s", buf); //如果输入1234567890,printf一样会输出显示,但是后面的67890会被写到别的非数组空间中,而一旦这部分空间恰巧被系统分配给其他地方使用,可能就会将其进行覆盖,即不再是67890,即此时printf输出的将可能不是67890
scanf_s("%s", buf, 6) //最后一个字节用于存放'\0'
  • 示例
#include <stdio.h>

int main(void)
{
	/*接收字符*/
	char c1, c2, c3;
	scanf_s("%c%c%c", &c1, 1, &c2, 1, &c3, 1); //scanf("%c%c%c", &c1,&c2, &c3);
	printf("c1 = %c\n", c1);
	printf("c2 = %c\n", c2);
	printf("c3 = %c\n", c3);
	/*接收整数*/
	int a1, a2, a3;
	scanf_s("%d %d %d", &a1, 1, &a2, 1, &a3,1); //需要用空格隔开,否则会有歧义,因为123可以当做一个是一个整数
	printf("a1 = %d\n", a1);
	printf("a2 = %d\n", a2);
	printf("a3 = %d\n", a3);
	/*接收字符串*/
    char str[6];
    scanf_s("%s", str, 6); //数组名本身就是地址,所以这里无需取地址符
    printf("str = %s", str);
	return 0;
}
  • 空格问题

在接收字符串时,无论是scanf还是scanf_s,除了敲击换行, 输入空格也会终止接收输入

  • 使用正则表达式接收空格
int main(void) {
	char str[11] = { 0 };
	scanf("%[^\n]", str); //表示将输入的数据,除了\n之外的都放入到字符数组str的内存地址中
	printf("res: %s", str);
	return 0;
}
goto
  • goto的标签可以在goto之后定义
#include <stdio.h>

int main(void)
{
	printf("1\n");
	goto tag;
	printf("2\n");
tag:
	printf("3\n");
}
  • goto仅在当前函数中有效(可以在一个函数的两个for之间跳转)
#include <stdio.h>

int main(void)
{
	int j = 1; //因为直接进入for,所以需要在for的外边进行初始化,否则打印的将都是自动生成的一个随机数(在goto之前初始化)
	for (int i = 1; i <= 5; i++) {
		if (i == 3) {
			goto tag;
		}
		printf("i = %d\n", i);
	}
	for (; j <= 5; j++) {
		tag:
		printf("j = %d\n", j);
	}
	/*
	i = 1
	i = 2
	j = 1
	j = 2
	j = 3
	j = 4
	j = 5*/
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值