c语言

环境搭建

  • windows 安装 Visual Studio编辑器,即可编译执行c代码
  • mac 安装Xcode编辑器,即可编译执行c代码
  • 如果不使用集成编辑器,则需要再电脑中单独安装gcc软件
    • 使用gcc测试c语言文件举例:
      • touch hello.c //新创建一个c语言文件后,输入相关c代码
      • gcc hello.c //使用gcc把c代码编译为可执行文件,会生成一个a.out文件
        • 也可以一次编译多个文件,示例:gcc xxx1.c xxx2.c
        • gcc h* //编译所有以h开头的.c文件
      • ./a.out //执行生成后的文件

概述

  • gcc语编译命令/大致过程
    • 预编译:
      • gcc -E xxx.c
      • 文件包含、宏定义
      • 把头文件和源文件编译到一个文件中
    • 编译:
      • gcc -c xxx.c
      • 把c的代码翻译成机器指令 gcc -c
      • 会生成xxx.o文件,注意并不是最终的可执行文件
    • 链接:
      • gcc xxx.o -o xxx2
      • 将xxx.o编译为可执行文件,-o xxx2:指定编译后的文件名
      • gcc会把标准库、第三方库中的函数和你的程序整合在一起
  • c语言加载过程
    • 程序一开始存储在磁盘上(数据和指令)
    • 运行的时候加载到内存中:数据区、代码区、堆、栈
  • main函数
    • 主函数,操作系统直接调用,一个程序只能有一个main函数
  • c语言不直接支持二进制的输入输出

数据类型

概述

  • 字符集
    • c/c++语言字符集由字符、数字、空格、标点和特殊字符组成
  • 标识符
    • 程序员在程序中定义的单词,它表示程序中的一些实体,如:变量名、函数名、对象名
    • 标识符由字符、数字、下划线组成
    • 第一个字符必须是字母或下划线
    • 大小写敏感

常用的数据类型

  • 基本类型
    • 整型
      • 短整型 short
      • 整型(默认) int
      • 长整型 long
    • 字符型 char
    • 实型
      • 单精度型 float
      • 双精度型(默认) double
  • 构造类型
    • 数组 - 相同类型的数据集合
    • 结构体 struct - 不同类型的数据集合
    • 共用型/联合体 union
    • 枚举型 enum
  • 指针类型
  • 空类型 void
  • 定义类型 typedef

基本数据类型

  • 整型
    • 整型的值可以是正的、负的或者是0,但必须是整数,负数在内存中是以二进制的补码形式存储的
    • 整型存储在计算机中占32位,最高位为符号位
    • long类型在64位系统下是8个字节,在32位系统下是4个字节
关键字 所占子节数 数的表示范围
int 4 -2{31} ~ 2{31} - 1
[signed] short [int] 2 -2{15} ~ 2{15} - 1
[signed] long [int] 4/8 -2{31} ~ 2{31} - 1
unsigned int 4 0 ~ 2{32} - 1
unsigned short [int] 2 0 ~ 2{16} - 1
unsigned long [int] 4/8 0 ~ 2{32} - 1
  • 实型
    • 实型也称作浮点型
    • c语言中浮点数包括float和double
    • 系统的默认类型是双精度浮点型double,在使用单精度浮点型float时,需要在数字后面添加f或F用以区分
关键字 所占子节数 数的表示范围 精确表示的数字个数
float 4 绝对值E-37 ~ E+38 7 ~ 8
double 8 绝对值E-307 ~ E+308 16 ~ 17
  • 字符型
    • 字符型用于存储字母和标点等字符
    • 字符在计算机中采用二进制的ASCII码来存储
    • char类型存储一个字符占用一个字节的存储空间
关键字 所占子节数 数的表示范围
[signed] char 1 -128 ~ 127
unsigned char 1 0 ~ 255
  • sizeof运算
    • sizeof是一个单目运算符,返回变量或类型的子节长度,以子节为单位
    • 一般格式为:sizeof(<数据类型>)或sizeof(<变量名>)或sizeof(<常量>)
char ss[10] = "hello";
char * sp = "hello";
// - sizeof(传入数组名时,得到的时该数组的字节长度)
printf("%d\n", sizeof(ss));//10
// - sizeof(传入指针名时,得到的是指针类型的字节个数)
printf("%d\n", sizeof(sp));//4或8

输出、输入

  • 格式化输出
    • 可以使用printf()将数据显示在屏幕上,几种基本数据类型的数据符号如下:
      • %d 以十进制方式输出有符号的整数int或short(short也可以用%hd输出), %2d表示在输出时该数字占用两个位置
      • %u 输出无符号整数
      • %o 以八进制方式输出整数
      • %x 以十六进制方式输出整数
      • %lu 以long型输出
      • %ld 输出无符号长整型
      • %f 以小数形式输出float
      • %lf 以小数形式输出double
      • %.lf 以指定精度的形式输出double,例如:%.5lf 即显示小数点后5位
      • %c 以单个字符形式输出char
      • %p 以指针形式输出字符型数据
      • %s 以字符串形式输出字符型数据
      • %e 以指数(科学计数法)形式输出字符型数据
    • 因在printf中方%有特殊含义,所以要输出%时需要写两个%,如:printf("%%\n");
    • printf("%03d\n", 1)//打印时占3个位置,不足3位的在前面用0补齐
    • printf("%.3d\n", 1)//打印时占3个位置,不足3位的在后面用0补齐
printf("int = %lu\n", sizeof(long));//输出long类型所占的字节数
char c = 'A';
printf("c = %c, c = %d\n", c,c);
printf("%.2f\n", 1.234);
  • 标准输入
    • scanf
      • 默认以空格作为分隔符
    • 在VS编辑器环境中有的函数加上_s更安全,比如:scanf_s
    • perror
      • 输出错误信息,可以捕获到所在环境的错误信息
int num = 0;
scanf("%d", &num);//把用户输入到值以十进制数字,赋值给num变量

常量

  • 常量也称字面量,内存空间是只读模式
  • 常量分类
    • 数值型常量
      • 整型常量
      • 实型常量
    • 字符型常量
  • c语言中整型常量分:十进制、八进制数(前面加0)、十六进制数(前面加0x)
printf("%d, %d, %d\n", 123, 0123, 0x123);
  • 实型常量-有两种方式表示
    • 十进制形式、由符号、数字和小数点组成
    • 指数形式,即科学表示法,由整数(或小数)、e(或E)、整数顺序组成,e(E)之前必须有数字
      • 如:1234e-5 表示1234 * 10{-5}, 1234e+5 表示1234 * 10{+5}
  • 字符常量
    • 计算机用ASCII码的形式来保存字符,其中65表示大写字符 'A'
    • c语言中,单引号中的表示字符常量,其中单引号是必不可少的
    • 换行符为 '\n'
  • 字符串常量
    • 字符串是用双引号括起来的字符序列
    • c语言中规定以字符 '\0'作为字符串的结束标志。在输出时不显示,只用来表示字符串的结束
      • 例如: "hello"在内存中的存储情况为:(hello\0)
      • 注意: 字符串 "1" 和字符 '1' 在内存中存储分别为 (1\0) (1)
      • 两个字符的数学运算,是安装其对应的ASCII码值来计算的,如 '1'+'0'
  • 技巧:把字符数字转成相应的整数数字,减字符 '0'即可,整数数字转字符时,加上字符 '0'即可
printf("%d\n", '1'+'0');//97

printf("%d\n", '1'-'0');//1
printf("%c\n", 1+'0');//1(字符型)
  • 转义字符
    • 以反斜线开头
    • \0 空字符
    • \n 换行符,换下一行
    • \r 回车符,回到下一行的开头
    • \t 水平制表符
    • \b 退格符
    • \' 单引号
    • \" 双引号
    • \\ 反斜线
  • 枚举型常量
    • 枚举类型的成员都是常量
    • 在枚举类型中,每个枚举常量代表一个整型值,默认从0开始,用户可以给枚举常量进行赋值
// 枚举型常量的一般形式
enum weekday { SUN, MON }

enum weekday { SUN=1, MON }
  • 符号常量 - 宏定义
    • 可以使用宏表示常量。
      • 例如:#define PI 3.1415926
      • #define是一个预编译指令,所有的预编译指令都是以#开头的
    • 行尾不能有分号
    • define前要有#号
    • 符号常量名最好使用大写
  • 零值
    • c语言中可以用以下三种方法表示零值
      • 整数0
      • 字符串结束符 '\0'
      • 空值NULL,通常用于指针操作,在c++中false也代表0值

变量

  • 变量的定义
    • 取值可变的量,内存空间是可读可写的
    • 变量的定义形式:(类型说明符 变量名标识符, 变量名标识符...)
      • 如:int num; long num2,num2;
    • 注意:根据编码规范,一行只允许定义一个变量,不推荐一行定义多个变量
  • 定义变量的要求
    • 必须以 ';'号结尾
    • 变量定义必须放在变量使用之前
  • 变量的初始化和赋值
    • 初始化就是在变量定义时给一个初始值
    • 变量被定义后,如果不给它初始值,那么它的值是一个随机值
  • 非同文件中,相互使用变量时,需要声明,或在xxx.h头文件中声明
    • 例如:extern int num;//extern可以省略
  • 普通的全局变量
    • 在函数外部定义,作用范围是程序的所有地方
    • 生命周期:程序运行的整个过程一直存在
    • 默认值0
  • 静态全局变量
    • 前面用 static 修饰
    • 只在当前文件有效,作用范围是程序的所有地方
    • 默认值为0
    • 生命周期:程序运行的整个过程一直存在
  • 普通的局部变量
    • 作用范围只在当前代码块中有效
    • 生命周期:调用函数时才为其开辟空间,函数结束即释放
    • 默认值是随机的
  • 静态局部变量
    • 前面用 static 修饰
    • 作用范围只在当前代码块中有效
    • 生命周期:第一次调用函数时,为其开辟空间,函数结束不释放
    • 默认值0

运算符和表达式

基本概念

  • 运算符
    • 算术运算符:+ - * / % ++ --
    • 关系运算符:< <= == > >= !=
    • 逻辑运算符:! && ||
    • 位操作运算符:<< >> ~ | ^ &
    • 赋值运算符:= 及其扩展
    • 条件运算符:?:
    • 逗号运算符:,
    • 指针运算符:* &
    • 求字节数运算符:sizeof
    • 特殊运算符:() [] -> .
  • 表达式
    • 表达式是由运算符、操作数和标点符号组成的
    • 表达式可以是一个单独的常量或变量
    • 表达式是有值的
    • 可以为表达式添加括号,称为表达式的嵌套使用
  • 运算符的优先级
    • 优先级相同时,按运算符结合性规定的方向处理
    • 从高到低:括号、增减量、指针、正负、逻辑非、算数、关系、逻辑、条件、赋值、逗号
    • 综合性分为两种
      • 左结合性(自左至右),例如算数运算符
      • 右结合性(自右至左),例如赋值运算符

基本运算符

  • 赋值运算符
    • 当赋值号两边类型不一致时,根据将右边类型按照左边类型转换
    • 在c语言中规定,任何表达式在末尾加上分号就构成语句
  • 算术运算符
    • 两个整数相除得,会把结果取整,最终得到一个整数
    • 取余运算:
      • 余数符号和被除数相同
      • 不允许对浮点数取余,没有意义
  • 自增、自减运算
int n = 0;
int n1 = n++; //先赋值后自增
int n2 = ++n; //先自增后赋值
  • 关系运算符
    • 关系运算符可以直接应用于基本数据类型,对于浮点数不要轻易直接用 '=='和 '!='比较大小
  • 逻辑运算符: &&与、||或、!非
  • 条件运算符:如:三目运算(1?2:3)
  • 位运算:
    • 按位与& 按位或|
    • 右移>> 低位移除,高位(逻辑右移补0,算术右移补符号位)
    • 左移<< 高位移除,低位补0
  • 计算机系统二进制中,左侧是高位,右侧是低位
  • 逗号运算符:,
    • 结果永远是最后表达式的结果

类型转换

  • 隐式类型转换
    • 隐式类型转换是由编译器完成的
    • c语言规定转换规则是由低级向高级转换
    • 赋值运算时,如果两边类型不同,将自动转换为和左边相同的类型
    • 当表达式中只有char short int类型时,全部成员将转成int类型参与运算
    • 当表达式中出现带小数点的实数时,全部成员将转成double类型参与运算
    • 当表达式中即有有符号的数,也有无符号的数时,将全部转成无符号的数参与运算
  • 类型转换都是临时转换,并不改变变量本身的类型
  • 显示类型转换
    • 又叫强制类型转换
    • 直接在要转换的数据前用括号加需要强制的类型
  • 转换浮点数时,会直接丢弃小数点后面的数据,如:int(1.6) //得1

逻辑控制语句

  1. 程序中语句的分类
  • 表达式语句
  • 函数调用语句:由函数名,实际参数加上;号组成
  • 空语句
    • 程序中最简单的语句,只有一个单独的分号
    • 可以用作空循环体
  • 复合语句:由一个或多个括在括号内的语句组成
  • 控制语句
    • 分支语句:if、switch
    • 循环语句:do while、while、for
    • 辅助控制语句:break、goto、continue、return

分支语句

  1. if语句
    • if(){}else{}
    • if(){}else if(){}else{}
  2. switch语句
switch(表达式)//int char 枚举
{
    case 常量表达式1://常量表达式的值必须做到次序不影响执行结果
        语句1;
        break;
    case 常量表达式2:
        语句2;
        // break; 如果这里不写break,当常量表达式2成立时,将不会再对后面的条件进行比较,直接执行后面的语句,直到遇到break跳出
    case 常量表达式3:
        语句3;
        break;
    default:
        break;
}

循环

  • 循环语句概述
  • while循环语句
  • for语句
  • do while循环
  • 辅助控制语句 break、goto、continue、return
  • 嵌套循环语句
printf("aa\n");
goto gotoName;
printf("bb\n");
gotoName:
printf("cc\n");

数组

  • 数组定义
    • 数据类型 数组名[常数表达式]
    • 定长数组:常量表达式的值必须在编译时已知,可以是整型常数或是const int值,也可以是常量表达式
    • 可以用宏指定数组的大小
    • 变长数组:可以不指定数组长度,或者长度可变
    • 数组名(常量)代表数组中第一个元素的地址,也是是数组所占内存块的首地址。在二维数组中数组名代表第一行的地址
    • 可以用sizeof(数组名)获取数组在内存中所占的字节长度
    • 初始化数据时,必须连续初始化,不能间隔空值
    • 如果不想给数组初始化值,可以用0清空数据
#define SIZE 5//用宏指定数组的大小
int arr[SIZE];//数组的长度为4

int arr2[2] = {1,2};
printf("%d\n", arr[3])//数组越界:这里不会直接报错,会打印出未知的内存地址,会产生安全问题

int arr3[] = {1,2,3}//初始化有值时,数组长度会根据初始化的值自动生成

int arr4[2] = {};//无初始化值的数组
arr4[0] = 1;

int arr5[SIZE] = {};
for(int i=0;i<SIZE;i++){
        printf("请输入一个整数:\n");
        scanf("%d", &arr5[i]);
}

int arrRow[m][n];//定义一个m行n列的二维数组
int arrRow2[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
int arrRow3[3][3] = {1,2,3,4,5,6,7,8,9};
int arrRow4[][3] = {1,2,3,4};// 内存分布:1 2 3 4 0 0,被认为是两行,123为一行,400为一行
  • 一维数组
  • 多维数组
    • 定义二维数组时可以省略第一维的值,编译器会根据元素的总个数进行分配空间
    • 不管是几维数组,在内存中都是按一行存储的

字符串

字符串定义

  • 字符串是位于双引号中的字符序列,在内存中实际上是字符数组
  • 在内存中以 '\0'结束所占字符比实际多一个
  • 字符数组中,字符串的字符存放在相邻的存储单元,每个字符占一个单元
  • 可以用memset清零字符数组,记得加头文件#include<string.h>
  • 定义字符数组时,应确保数组长度比字符串长度至少多1
  • 未被使用的元素均被自动初始化为0
  • 被初始化的字符数组可以省略长度,默认为字符串长度加1
char carr[] = {'a','b','c'};
char carr2[] = "abc";

字符串的输入和输出

  • 格式化输入
    • 可以用scanf()接收字符串,它不接受空格,格式:scanf("%[n]s",字符数组名); n可指定要接收的字符串长度
    • 用scanf()数组字符串时,字符数组名前不加&
  • 其他输入函数
    • 输入字符串函数gets(),它可以接收空格,以回车结束输入。
      • 不安全的函数,容易造成字符数组越界
      • 对应有 puts()输出
    • 输入字符函数
      • getchar() 输入回显
      • getch() 输入不回显,可以接收 '\r',常用于输入密码操作,需要#include<conio.h>
        • 注意conio.h并不是标志库头文件
      • 对应有 putchar() 输出字符
char carr[] = "";

char str[10] = {0};
/*printf("请输入字符串\n");
scanf("%s", str);
printf("%s\n", str);*/

printf("请输入字符串\n");
gets(str);//接收用户输入,用户实际输入的字符串长度有可能,比str的长度大,所以这是不安全的操作
puts(str);//输出字符串到屏幕
putchar(str[0]) //输出字符
putchar('\n');

// 字符串输出时,会以找到\0为结束标志,如果找不到\0会把内存中其他的字符也输出,直到找到\0。例如:
char str1[] = "how are you?";
char str2[] = "I'm fine.";
str2[9] = ' ';//这里如果修改了str2的结束标志
puts(str2);//这里打印时,会导致str2的结束标志找到str1的结束标志 //I'm fine. ?o

字符串操作函数

  • 需要#include<string.h>
    • size_t代表无符号整型,他是在库头文件中用typedef定义出来的
    • size_t strlen(const char * s);
      • 统计字符串长度,返回字符串中的字符个数
      • 检查 '\0',没遇到就加1,遇到就结束(返回值不含 '\0')
    • 内存赋值函数 void * memset(void * ptr, int value, size_t num);
      • 也称空间设定函数
      • 将ptr指向的内存空间的num个字节全部赋值为value
      • 返回目的内存的首地址,即ptr的值
    • 字符串拷贝 char * strcpy(char * dest, const char * src);
      • 格式: strcpy(目的str1, 源str2);/需要保证str1空间够用(比str2大),否则不安全,造成内存污染
      • 目标字符串必须是变量,不能是常量
      • '\0'也会被拷贝进去
    • 字符串拷贝 char * strncpy(char * dest, const char * src, size_t n);
      • 不检查目标字符串的大小,当目标字符串内存不足时,会导致崩溃
      • n:指定拷贝的字符个数
      • 不拷贝 '\0'
      • 如果n大于src指向的字符串中字符的个数,则在后面填充n-strlen(src)个'\0'
    • 字符串比较 int strcmp(const char * s1, const char * s2);
      • int res = strcmp(str1, str2)
      • 比较相同位置上的ASCII码值的大小(常用于比较字符串是否相等)
      • 相等返回0,str1大于str2返回正数1,str1小于str2返回负数-1
    • 字符串比较 int strncmp(const char * s1, const char * s2, size_t n);
      • 比较两个字符串的前n个字符
      • 比较相同位置上的ASCII码值的大小
      • 相等返回0,s1大于s2返回正数1,s1小于s2返回负数-1
    • 字符串拼接 char * strcat(char * dest, const char * src);
      • 先删除dest后的串标志'\0',把src中的字符连接到dest的后面,src的'\0'也会拼接进去
      • 返回值是dest的首地址
    • 字符串拼接 char * strncat(char * dest, const char * src, size_t n);
      • 追加str指向的字符串的前n个字符,到dest字符串的后面
      • 会追加 '\0'
      • 如果n大于src的字符个数,不回补多余的 '\0'
    • 字符串查找 char * strchr(const char * s, int c);
      • 字符匹配
      • 在字符串中找ASCII码为c的字符,可用于检测是否包含某个字符
      • 注意是首次匹配,返回找到的字符的地址,找不到返回NULL
    • 字符串查找 char * strrchr(const char * s, int c);
      • 字符匹配
      • 在字符串中找ASCII码为c的字符,可用于检测是否包含某个字符
      • 注意是末尾匹配,返回找到的字符的地址,找不到返回NULL
    • 字符串匹配函数 char * strstr(const char * haystack, const char * needle)
      • 在haystack指向的字符串中查找needle指向的字符串
      • 首次匹配,返回找到的字符串的地址,找不到返回NULL
    • 字符串切割 char * strtok(char * str,const char * delim);
      • 按照delim指向的字符串中的字符,切割str指向的字符串,即在str指向的字符串中匹配delim中的字符将其变成'\0'
      • 调用一次切割一次,想继续切割时再次调用的时候第一个参数传NULL表示接着上次切割的位置继续切割
      • 遵循首次匹配原则
      • 返回切割后的字符串地址,如果未匹配上表示已经不能再用该字符切割了,则返回NULL
  • 需要#include<stdlib.h>
    • 字符串转换函数,
      • 转成浮点型 double atof(const char * nptr);
      • 转整型 int atoi(const char * nptr);
        • 转换失败返回0
      • 转long型 long atol(const char * nptr);
  • 需要#include<stdio.h>
    • 格式化输出 int sprintf(char * buf, const char * format,...);
      • 输出到buf指定的内存区域
      • 例:sprintf(str, "%d", 123);//利用该函数,可以把数字123转成字符串赋值给str变量
    • int sscanf(char * buf, const char * format,...);
      • 默认以空格作为分隔符
      • 从buf指定的区域读取数据
      • 跳过数据语法:%*s、%*d
      • 指定宽度语法:%[字节个数]s
      • 集合操作语法(只支持获取字符串) %[类正则语法]
        • 例:%[a-z] 表示匹配a到z的任意字符,尽可能多的匹配,遇到不在a-z的字符即停止获取
  • 需要#include<ctype.h>
    • 字符测试函数,
      • 判断字符是否是数字 isalnum
      • 判断字符是否是字符 isalpha
char str[] = "98876j54s3x2t";
memset(str, '\0', 5);//把str前5位的元素清零
memset(str, 'A', 5);//把数组前5位的元素替换为A
memset(str+5, 'A', 5);//从数组下标为5的元素开始往后5个元素都替换为A
//拷贝
strcpy(str, "bb");

//字符查找------
char * sp = "abcdefg";
char * p = strchr(sp, 'd');//注意:这里的字符'd'会转成其对应的ASCII码值传进去
if(p != NULL){ printf("sp 中 包含 'd' 字符"); }

//字符串查找
char s1[100] = "abc&&def&&g";
char s2[100] = "&&";
char * p = strstr(s1, s2);
if(p != NULL){
        printf("p-s1 %ld\n",  p - s1);
}

//字符串切割------
char str[100] = "aaa;:bb,:;.cc.,dd";
char * p[10];
printf("%s\n", str);//aaa;:bb,:;.cc.,dd
p[0] = strtok(str, ":;.,");//只要命中":;.,"里面的任意一个字符即可
printf("p0 = %s\n", p[0]);//aaa
printf("%s\n", str);//因为第一个匹配到的;被换成了\0所以,打印出来的是aaa
while(p[i] != NULL){
        i++;
        p[i] = strtok(NULL, ":;.,");//当遇到类似,:;.连续的几个在匹配项中的字符时,只把第一个匹配的字符转成\0,而且下次匹配时会忽略后面紧跟的几个字符
}
for(int j = 0; j < i; j++){
        printf("p[%d] = %s\n", j, p[j]);
}
// p[0] = aaa
// p[1] = bb
// p[2] = cc
// p[3] = dd

//格式化字符串------
char ss[100];
sprintf(ss, "%d-%d-%d", 2023,1,2);
printf("%s\n", ss);//2023-1-2
sscanf("2023-01-02", "%d-%2d-%2d", &a, &b, &c);
printf("%d %02d %02d\n", a, b, c);//2023 01 02
char ss2[32];
sscanf("123 456", "%*d%s", ss2);//跳过123,将456赋值给ss2
sscanf("12345678","%2s", ss2);//把"12345678"中的前两个字符赋值给ss2
sscanf("123a45b6c78","%[^ac]", ss2);//碰到a/b字符就停止获取
sscanf("ddd12&name=zhangsan&age=18","%*[^&]&%[^&]", ss2);//获取两个&符之间的字符,解释:跳过第一个&之前的字符,再隔一个&,再取下一个&符之前的字符

函数

函数的基本概念

  • 函数的定义:把一些功能相同的程序分割成一个个程序块,封装起来
  • 函数不能嵌套定义
  • 允许递归调用自己本身
  • 外部函数
    • 可以在程序的任何文件中使用
  • 内部函数
    • 返回值类型前加 static 修饰的函数
    • 只在当前文件中有效

函数 的定义和参数

  • 无参函数
    • 返回值类型 函数名(){...}
  • 有参函数
    • 返回值类型 函数名(参数或参数列表){...}
  • 函数的定义说明
    • 函数名首字母必须大写,每个单词的首字母也大写,最好用下划线分割
    • 函数的声明和定义可以分开
    • 先声明再使用
    • 函数可以被多次重复声明,但是只能定义一次
  • 函数的参数
    • 分为形参和实参
    • 实参出现在主调函数中,进入被调函数后,实参变量不能使用
    • 形参变量只有在被调用时才分配内存单元
    • 实参和形参占据不同的存储单元
    • 函数默认采用值传递
    • 实参可以是常量、变量、表达式、函数,但是在传递前,必须有确定的值
  • 函数的返回值
    • 函数返回值类型和函数定义时的返回值类型应保持一致,如果不一致,则以函数定义时为准,自动进行类型转换
    • 无返回值的函数,应定义返回值为空类型,如果不写返回值类型默认为int
char[] str = "abc";//错误,没有char[]这种写法
char str[] = "abc";//正确
char* str = "abc";//正确

函数的应用

  • 程序的内存区域
    • 代码区:存放程序的代码和各个函数的代码块
    • 数据区:存放程序的全局变量和静态变量、常量(不包括define定义的数据)
    • 栈区:存放程序的局部变量,内存小,连续存放,先进后出。在一个变量前加上static后,这个变量将不占用栈内存空间,会放在数据区
    • 堆区:存放动态数据,需要用指针访问,内存大,不连续
  • 局部变量
    • 局部变量也称内部变量,在函数内部使用,不能被该函数外的代码使用
    • 函数调用结束后,局部变量所占的内存自动释放
    • 局部变量应尽量定义在程序的开头
    • 如不初始化,它的值将是随机的
  • 全局变量
    • 对整个程序都是可见的
    • 它不属于某个函数,而属于整个源文件
    • 一般在main()函数之前声明全局变量
    • 全局变量有效区域就是从它定义开始到文件结束
    • 如不初始化,系统自动初始化为0
  • 静态局部变量
    • 在局部变量前加static
    • 只被初始化一次,在第一次进入该函数时创建,退出函数时保留其值
    • 静态局部变量和全局变量一样,系统会默认初始化为0
  • 函数与数组
    • 数组名作为实参传递:由于数组名就是数组的首地址,所以这实际上是地址传递,即修改形参数组中的内容将相应的改变实参数组中的内容
#include<stdio.h>//使用尖括号引入的是标准库中的头文件
// #include"my-init.h" //使用双引号引入的是自定义的当前文件夹中的头文件
extern int global; //extern 表示该变量已经在其他文件中定义了
int main(){ }

// 函数默认都是可以全局跨文件使用,但是如果在函数前面加上static,则该函数只能在当前文件调用 
static void Hello(){};

// 计算数组长度技巧
long l = sizeof(arr)/sizeof(arr[0])
void print_arr(int arr[]){
        //在函数内部,不能用于计算参数数组的长度, 因为数组作为参数传递给函数后,数组退化成指针,只代表数组的首地址
        //这里sizeof(arr)返回的只是指针的大小,在64位系统中,所有的指针大小都是8字节
        printf("size = %lu\n", sizeof(arr)/sizeof(arr[0]));//错误写法
}

函数递归调用

  • 求阶乘、求斐波纳列数据...

const 关键字

  • const 修饰函数参数
    • 表示函数内部不能对函数的参数进行修改,只读
  • const 修饰函数返回值
    • 对于指针类型的返回值,如不希望通过返回的指针修改该指针指向的变量值,需要在返回值前加const修饰
  • 修饰普通变量:只读模式

指针

指针的概念

  • 指针就是地址, 可以认为就是内存中存放地址编号的容器/变量
  • 指针变量:存放变量地址的变量,书上或程序中所说的指针一般指的是指针变量,指针类型的变量占4个字节
    • 对应类型的指针变量,只能存放对应类型的变量地址。如:整型指针变量只能整型变量地址,但是通用指针(void *)可以保存任意类型的变量地址
    • 扩展:
      • 字符变量(char ch = 'a'),ch占一个字节只有一个地址编号,这个编号就是ch的地址
      • 整型变量(int n = 0x12345678),n占4个字节占有4个存储单元,有4个地址编号。规定第一个编号代表n的地址
  • 使用指针的目的:通过指针能够找到被值的变量,或者说要通过指针间接访问到被值的变量
  • 变量的值和变量的地址-指针运算符
    • 地址运算符:& 取地址
      • 后面跟一个变量名时,&给出该变量的地址
    • 间接运算符 * 取值
      • 后面跟一个指针名称或地址时,给出存储在被指向地址中的数值
    • 示例:
      • int* p = &bank;//将bank变量的地址给p
      • *p = bank//将bank的值放入p地址所对应的内存中,*p等价于p指向的变量
  • 指针类型
    • 整形指针、字符指针、实型指针、数组指针...
  • NULL
    • 空指针,哪里也不指向,可以认为其指向内存编号为0的存储单位
    • 一般用于给指针初始化,如:int * np = NULL;
int num = 0;
int* p = &num;
printf("*p = %d, p = %p\n", *p, p);
*p = 2;//通过指针修改了它指向的普通变量的值
printf("num = %d\n", num);

//指针取值时最终取几个字节数据,是右指针的类型决定的,指针的类型占几个字节,就取几个字节,如:
int n2 = 0x12345678;
char* p2 = (char*)&n2;//不同类型时,需要强制转换,这里p2指向的是n2的第一个字节的地址
printf("%0x\n", *p2);//打印出的是(n2的第一个字节数)78,因为char占1个字节,而int占4个字节

指针变量的定义

  • 一般形式为:类型*指针变量名
    • 一个*号代表一个指针
    • 一个类型的指针只能指向同类型的变量
    • 指针也可以被声明为全局、静态、局部
  • 指针变量的初始化
    • 指针变量使用之前必须赋予具体的值,并且只能赋地址
    • 如果一个指针在定义后没有初始化,应该给它赋予一个空值,避免出现使用未被初始化的指针引起的系统混乱
  • 指针变量的引用
    • 通过指针间接访问变量,使用*来表示取地址的内容
    • void类型的指针,可以指向任何的变量,但是在使用时,需要进行强制类型转换后才能使用
  • 所有的指针大小(地址编号 ),在64位系统中都是8字节,在32位系统中都是4字节

指针运算

  • 指针与整数的加减运算
    • pnid +/- n <===> pnid +/- n*sizeof(指针指向类型)
  • 指针和指针的减法运算
    • 指针和指针之间只有减法运算,没有其他运算
      • pnid1 - pnid2 = (pnid1 - pnid2) / sizeof(指针指向类型)
    • 两个指向同一个数组元素的指针,相减的结果是两个指针中间相差的元素个数
  • 指针之间的比较运算
    • 其实就是指针地址值的比较
    • 相同类型的指针指向同一个数组元素时,指向前面元素的指针小于指向后面元素的指针
  • 根据指针类型不同在运算时,移动的字节数也不同
    • 如:int* p = &n; 在自增运算时,每次p++以4个字节为单位向前移动

指向数组元素的指针

  • 数组名访问数组元素
    • 数组名(是一个常量)保存的是数组首元素的地址编号,其实就是一个指向该数组中第一个元素的指针
    • 在实际的数组元素前写一个 '&'符
    • 写一个表达式,在数组名后面加元素偏移量
  • 用指向数组的指针访问数组元素
    • 数组的地址赋值给一个指针,让指针指向这个数组
  • 如果指针变量p已指向数组中的一个元素,则p+1指向数组中下一个元素
// 使用指针方式访问数组元素
int arr[5] = {1,2,3,4,5};
int *p = arr;//这里p和arr不同,p是指针变量,arr是常量,可以给p赋新地址,不可以给arr赋新地址
int n1 = *(arr);
int n2 = *(p++);
printf("n1=%d, n2=%d\n", n1, n2);

// 指向二维数组的指针
int arr2[2][3] = {1,2,3, 4,5,6};
int (*p)[2][3] = &arr2;

字符指针和字符串指针

  • 指针字符串的表示形式:char *pStr = "hello";
    • 注意pStr指向的是字符串变量的首地址
    • 指针形式的字符串,指向的内容是不可变的,但可以重新赋值
    • 字符数组形式的字符串,不能直接重新赋值
  • 字符串指针变量与字符数组
    • 字符数组和字符指针都可以存储和运输字符串
    • 数组名是常量,存放的是以 '\0'结束的字符串;指针名是变量,存放的是字符串的首地址
//字符数组-字符串 - 内容可修改,不能对变量重新赋值
char str[32] = "language";//在内存(栈/静态全局区)中开辟了一段空间存放字符串
//str = "hello word";//错误,不能重新赋值
strcpy(str, "hello word");
char * ps = &str;//指针指向字符数组。注意:指针不能指向用const修饰的数组

//指针-字符串 - 内部不可修改,可以对变量重新赋值
char * pstr = "aa";//在文字常量区开辟了一段空间存放字符串,将字符串首地址赋给了ps指针变量。
char * pstr1 = "language";//注意pstr1只有1个字节空间存放的不是整个字符串内容,而是字符串首字符地址编号
pstr1 = "hello world";//正确-修改了p的地址,指向了新的地址
//strcpy(pstr1, "hello world");//错误-不能修改内容

char *p2;//未初始化的指针,也叫野指针
// strcpy(p2,"aaa");//使用野指针,会出现未知的运行时问题

//存放在堆区的字符串 - 内容可以修改,可以对变量重新赋值
char * pstr2 = (char*)malloc(10);//在内存堆区,动态申请了10个字节空的存储空间,首地址赋给了pstr2
strcpy(pstr2, "hello");//给动态申请的内存指针赋值
// 10 * sizeof(int) //40个字节

const 指针

  • const int* xxx 和 int const* xxx 两者意义是相同的
    • 可以指向const对象,也可以指向非const的对象
    • 不能通过间接符*,修改const指针所指向的对象的内容
    • 可以重新赋值新的地址,但不可修改指针指向的内容
  • int * const xxx
    • 这种指针必须初始化
    • 指针本身的地址不可修改,但是它指向的内容可以修改
  • const int * const xxx
    • 这种指针综合了上述两种指针的特性,既不可以更改所指向的地址,也不可以修改所指向的内容
const int SIZE = 5;
int * const p2 = &SIZE;
*p2 = 10;//虽然p2指向的内容是可以改的,但是SIZE是只读数据,所以这里会发警告,单不会报error错误
printf("size = %d, *p2 = %d\n", SIZE, *p2);//虽然SIZE对应的地址中的数据被修改了,但是因为在预编译时,会做常量优化,把所有用到SIZE的地方直接替换为5,所以这里打印的是:size = 5, *p2 = 10

指针的指针

  • 定义:指针在内存中也有地址编号,存放指针地址的指针变量称为指针的指针
  • 对指针取地址得到的就是一个指针的指针
int n = 1;//变量n在内存中有地址编号
int * p = &n;//指针p在内存中也有地址编号
int * * pp = &p;//pp就是用来存放p指针地址编号的指针  
printf("%d\n", **pp);//1

void swap(char * * ppp){
        *ppp = "world";//改变外部指针的值
}
int main(){
        char * p = "hello";//指针字符串内容存放在文字常量区
        swap(&p);//传入指针的地址,即指针的指针
        printf("p = %s\n", p);
        return 0;
}

指针数组

  • 指针数组是数组元素都是(相同类型)指针变量的特殊数组
    • 格式:类型名称 * 数组名称[数组长度]
int n = 10;
int* parr[5] = {&n};
char * names[3] = {"aa","bb","cc"};//常用形式,用字符指针数组,代表字符串数组
// names[0] 存放的是"aa"首字符的地址编号,names[1] 存放的是"bb"首字符的地址编号... 

数组指针 - 指向一维数组的指针

  • 指向数组的指针定义如下:类型名称 (*指针名)[数组长度]
  • 数组指针的长度不能省略
  • 数组长度、元素类型必须与指针定义时给出的长度、类型相同
  • 指针指向的是整个数组,而不是数组元素
    • 数组地址和数组首地址,地址相同,长度不同
int (*p)[5];//定义了一个数组指针p,p指向的是整型的有5个元素的数组

int a[3] = {1,2,3};
int (*arrP)[3] = &a;
//arrP[0]、*(arrP+0)、(*arrP)[0]、arrP[0][0] //访问数组

指针与二维数组

  • 数组指针注意配合二维数组来使用
  • 在二维数组中,数组名其实就是一个数组指针,因为它存储的是二维中第0个一维数组的地址
  • 访问二维数组的指针有三种形式
    • 指向普通元素的指针
    • 指向一维数组的指针
    • 指向二维数组的指针
  • 数组指针运算:指针+1相当于指针+1*sizeof(数组名),即跳到二维中的下一个一维数组
  • 实际应用中:一维数组指针常配合二维数组使用,二维数组指针常配合三维数组使用...
int arrNum[2][2] = {1,2,3, 4,5,6};
int *p = &arrNum[0][0];//指向普通元素的指针
int (*p2)[3] = &arrNum[0];//指向一维数组的指针,其实和int * p = arrNum;是一样的
// printf("1-0 = %d\n", (*(p2+1))[0]);//4
int (*p3)[2][3] = &arrNum;//指向二维数组的指针

// int * p[5]是指针数组(p是一个数组,数组中的每一项都是一个指针)
// int (*p)[5]是数组指针,p是一个指针指向的是代表某个一维数组的地址

//数组名取地址,是个数组指针,+1向后跳一个数组
int a[10];//a是数组的首地址,&a是个数组指针
// a+1 跳一个整型元素,4个字节
// (&a)+1 跳一个数组,40个字节
int a2[4][5];//a2在二维数组中代表的是第0个一维数组的地址,其实是数组指针,所以&a2就变成了二维数组指针
// &a2 和 (&a2)+1 之间相差了4*5*4=80个字节的距离
  • 数组名和指针的区别
    • 相同点
      • 数组名保存的是数组首地址,指针保存的也是地址
      • 可以把数组名直接赋值给一个指针变量,这样这个指针就指向了该数组的首地址
    • 不同点
      • 数组名是常量,指针是变量
      • 对数组名取地址得到的是数组指针,对指针取地址得到的是指针的指针
  • 数组指针取*
    • 数组指针取*,并不是取值的意思,而是指针的类型发生了变化
    • 一维数组指针取*,结果为它指向的一维数组第0个元素的地址,它们还是指向同一个地方
    • 二维数组指针取*,结果为一维数组指针,它们还是指向同一个地方
    • 三维数组指针取*,结果为二维数组指针,它们还是指向同一个地方
    • ...多维以此类推

函数指针

  • c语言规定,函数的名字就是函数的首地址,即函数的入口地址
  • 定义函数指针:数据类型(*指针变量名)(形式参数列表)
  • 把函数首地址赋给指针变量时,直接写函数名称即可,不用写括号和函数参数,这个变量就称为函数指针变量
  • 函数指针其实就是保存函数地址的变量,方便在其他地方调用该函数
  • 利用指针变量调用函数时,要写明函数的实际参数
float(*p1)(int x);//定义了一个指向函数的指针变量p1
float *p2(int x);//声明了一个函数p2,返回值为一个float类型的指针

int add(int a, int b);
int sub(int a, int b);
int main(){
        int (*p)(int,int) = add;
        printf("res = %d\n", p(10, 20));
        p = sub;
        printf("res = %d\n", (*p)(10, 20));//通过函数指针调用函数时,可以加*也可以不加

        // 批量调用函数
        typedef int (*funcPointer)(int, int);//自定义函数指针类型
        funcPointer funArr[2] = {add, sub};
        //简写:int (*funArr)[2](int,int) = {add,sub};
        for(int i = 0; i < 2; i++){
                printf("arr-res = %d\n", funArr[i](20, 10));
        }
        return 0;
}
  • 函数指针数组:由若干个相同类型的函数指针变量组成的,在内存中按顺序存储的数组
    • 类名 (*数组名[元素个数])(形参列表)
void fn1(int x, int y){}
void fn2(int x, int y){}
void (*pfa[2])(int,int) = {fn1, fn2};

指针变量作为函数参数

  • 指针变量以实参的形式传递给函数,可以在函数中改变实参的值
  • 指针参数,传递实参时应该传递变量地址
  • 数组名可以作函数的实参和形参
void swap(int *a, int *b){
        int temp = *a;//*加地址 取值
        *a = *b;//*加地址 = xxx 设置值,设置的是外部传入的变量的值,如果写 a = xxx 只是改变了a指针的值并没有改变指针指向地址的值 
        *b = temp;
}
// void testArr(int p[]){
void testArr(int * p){//一维数组参数
    // int p[]和int * p 在这里编译器认为是的等价,都会当作整型指针处理
    *(p +1) = 5;

}
// void testArr2(int p[][2]){
void testArr2(int (*p)[2]){//二维数组参数
// int p[][2]和int (*p)[2] 在这里编译器认为是的等价,都会当作整型数组指针处理
    p[0][1] = 100;
}
// void testFn(char * p[]){
void testFn(char ** p){//指针的指针参数
// int * p[]和int ** p 在这里编译器认为是的等价,都会当作指针的指针处理
   p[0] = "cc";
}
int main(){
        int x = 10;
        int y = 20;
        swap(&x, &y);//交换x和y的值
        printf("x = %d, y = %d\n", x, y);//20,10

        int arr[3] = {1,2,3};
        testArr(arr);//数组名本身就是指针
        printf("arr[1] = %d\n", arr[1]);

        int arr2[2][2] = {
            {1,2},
            {3,4}
        };
        testArr2(arr2);//二维数组的名字指向的是第一行的数组,是数组指针
        printf("arr2[0][1] = %d\n", arr2[0][1]);

        char * strs[2] = {"aa", "bb"};//strs[0]、strs[1]是char*
        testFn(strs);//strs就是&strs[0] 是char**
        printf("strs[0] = %s\n", strs[0]);
        return 0;
}

指针函数

  • 返回值是一个指针的函数,称为指针型函数
  • 不可以返回局部变量的地址,会报警告,可返回局部静态变量的地址
char* testFn(){
//    static char sts[100] = "hello world";
//    return sts;//返回静态局部数组地址

//    char * s3 = (char*)malloc(100);
//    strcpy(s3, "hello");//strcpy不安全,在Visual Studio中推荐用strcpy_s
//    return s3;//返回动态堆内存地址

   char * s2 = "aa";
   return s2;//返回文字常量区的字符串地址
}
int main(){
        char * ss = "aa";
        ss = testFn();
        printf("ss = %s\n", ss);
        return 0;
}

api

  • system
    • 可通过system方法执行系统shell命令
    • 如:system("cls") 清空屏幕

stdlib.h

  • 随机数
    1. srand((unsigned int)time(NULL))
      • 以当前时间为准,设置随机种子
      • 如果不设置随机数种子,每次获取的随机数将都是一样的
    2. rand()
      • 生成随机数

time.h corecrt.h

  • time(NULL)
    • 获取当前时间戳,单位秒
    • time_t st = time(NULL);//时间戳是long long类型的数据,所以需要用corecrt中的time_t类型

conio.h

  • getch()
    • 接收键盘输入值, 不回显

关键字/修饰符

  • signed 有符号的
  • unsigned 无符号的
  • register 用于修饰的变量是寄存器变量
  • static 静态的
  • const 常量、只读
  • auto 自动的;
    • 修饰的变量,是具有自动存储器的局部变量,可以省略;
    • 进行类型推导,即根据变量的初始化表达式自动推断变量的类型
  • extern 外部的,一般用于函数或全局变量的声明
  • typedef 给一个已有的类型,重新起个类型名
  • volatile 用volatile定义的变量是易改变的,即高速cpu每次用该变量时都要从内存中取不要使用寄存器中的备份,保证是最新的值

变量的存储类别

内存的分区

  • 内存:物理内存、虚拟内存
    • 操作系统会在物理内存和虚拟内存之间做映射
    • 写应用程序时,看到的都是虚拟内存
  • 在程序运行时,操作系统会将虚拟内存进行分区
    • 堆:动态申请内存时,会在堆里开辟空间
    • 栈:主要存放局部变量
    • 静态全局区
      • 未初始化的静态全局区
        • 未初始化的静态变量、全局变量
      • 初始化的静态全局区
        • 静态变量、全局变量
    • 代码区:存放程序代码
    • 文字常量区:存放常量

预处理、动态库、静态库

c语言编译过程

  1. 预处理/预编译 gcc -E hello.c -o hello.i
  • 将.c中的头文件展开,宏展开,宏替换
  • 生成.i文件
  • 不检查语法
  1. 编译 gcc -S hello.i -o hello.s
  • 将.i文件编译成.s汇编文
  • 检查语法
  1. 汇编 gcc -c hello.s -o hello.o
  • 将.s文件生成.o二进制文件
  1. 链接 gcc hello.o [可同时链接多个文件] -o xxx
  • 将所有的.o文件链接成目标文件,即可执行程序

预处理种类

  • #include
    • #include<> 在系统指定的路径下找头文件。(linux系统头文件默认路径:/usr/include,库文件默认路径:/usr/lib)
    • #include"" 依次在 当前目录、系统指定路径 下找头文件
    • include也可以用于包含.c文件,但是不建议这样做,因为include包含的文件会在预编译时被展开,如果同一个.c文件被包含多次,就会被展开多次,容易导致函数的重复定义
  • #define
    • 宏定义,结尾不需要加分号
    • 作用范围:从定义的地方到本文件末尾
    • #undef 终止宏
  • #pragma
    • 该指令用于指定计算机或操作系统特定的编译功能
    • 如:#pragma warning(disable:4996)
      • 在c文件开始处写上这句话即告诉编译器忽略4996警告
      • 在VS编辑器中使用strcpy、scanf等一些不安全的标准c库函数会报4996错误,可以用该方法屏蔽
#define PI 3.14 //不带参数的宏
#define S(a,b) a*b //带参数的宏,
// 宏替换过程:S(2,3)-> a*b -> 2*3
// 宏替换过程:S(1+2,3)-> a*b -> 1+2*3 注意这里不是3*3而是1 + 2*3,这是宏替换的副作用,可以在宏定义时用括号来避免,如:#define S(a,b) ((a)*(b))
//带参宏和带参函数的区别:带参宏参数无类型,被调用多少次就会被展开多少次,执行代码时不需要压栈和弹栈,浪费空间节省时间;带参函数参数有类型,代码只有一份存在代码区,调用的时候需要压栈和弹栈,浪费时间节省空间
  • 选择性编译
    • #ifdef xxx #else #endif
      • 只要xxx被定义了即认为有效,否则走else逻辑
    • #ifndef xxx #else #endif
      • 和#ifdef相反
    • #if 表达式 #else #endif
      • 只有当表达式的值为真或表达式的计算结果为真时才认为有效,否则走else逻辑
//在头文件中使用选择性编译
#ifndef __XXX__
#define __XXX__
extern inf fun();
#endif

静态库

  • 动态编译
    • gcc hello.c -o hello
    • 使用的时动态库文件,只建立链接关系,在运行时动态地加载库文件的内容
  • 静态编译
    • gcc -static hello.c -o hello
    • 使用的是静态库文件,会将库文件内容打包到可执行文件中,编译后体积较大
  • linux系统下制作静态库
/*
- 制作
    1. 编写mylib.c
    2. gcc -c mylib.c -o mylib.o
    3. ar rc libmylib.a mylib.o
        - libmylib.a 为固定格式lib<自定义名>.a
- 编译使用自己制作的静态库 mytest.c
    - 静态库和自己的程序代码在同目录下时:gcc -static mytest.c libmylib.a -o mytest
    - 不在同目录时,需要指定目录:假如静态库放到了/home/a目录下,静态库头文件放到了/home/h
        - 格式:gcc -static mytest.c -o mytest -L<静态库路径> -l<静态库文件名去掉lib和.a> -I<静态库头文件路径>
        - 示例:gcc -static mytest.c -o mytest -L/home/a -lmylib -I/home/h
    - 把自定义的静态库放到系统默认路径下时
        - gcc -static mytest.c -o mytest -l<静态库文件名去掉lib和.a>
        - 示例:gcc -static mytest.c -o mytest -lmylib
*/
  • linux系统下制作动态链接库
/*
- 制作
    1. 编写mylib.c
    2. gcc -shared mylib.c -o libmylib.so
        - libmylib.so 为固定格式lib<自定义名>.so
- 使用
    - 同目录
    1. 添加环境变量:export LD_LIBRARY_PATH=./:SLD_LIBRARY_PATH
    2. gcc mytest.c libmylib.so -o myetst
    - 非同目录,假如动态库和头文件放到了/home/test
    1. 添加环境变量:export LD_LIBRARY_PATH=/home/test:SLD_LIBRARY_PATH
        - 格式:gcc mytest.c -o mytest -L<动态库路径> -l<动态库文件名去掉lib和.so> -I<动态库头文件路径>
        - 示例:gcc mytest.c -o mytest -L/home/test -lmylib -I/home/test
    - 系统默认目录
        - gcc mytest.c -o mytest -l<静态库文件名去掉lib和.so>
        - 示例:gcc mytest.c -o mytest -lmylib
*/

main函数

  • mian函数参数
    • 第一个参数是实参个数
    • 第二个参数是实参指针数组
// 打印程序执行时传入的参数
int main(int argc, char* argv[]){
    printf("argc = %d\n", argc);
    for(int i = 0; i < argc; i++){
        // argv[0] 是当前文件相对路径,后面依次为传入的参数
        printf("argv[%d] = %s\n", argv[i]);
    }
}

动态内存分配

  • 静态内存分配
    • 在程序编译或运行过程中,按事先规定大小分配内存空间。如:int n[10];
    • 分配在栈区或全局变量区,一般以数组的形式呈现
  • 动态内存分配
    • 在程序运行过程中,根据所需大小自由分配空间
    • 分配在堆区,一般使用特定的函数进行分配

动态分配函数

  • stdlib.h 依赖库
    • void * malloc(unsigned int size);
      • 在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型,函数原型返回void*指针,使用时必须做相应的强制类型转换
      • 申请的内存空间内容是随机的,不确定的,一般使用memset初始化
      • 返回值:
        • 成功,返回分配空间的起始地址,这块空间可以认为是指定长度的数组
        • 失败返回NULL
      • 如果多次用malloc申请内存,第1次和第2次申请的内存不一定是连续的
    • void free(void * ptr)
      • 释放ptr指向的内存(必须是malloc/calloc/realloc动态申请的内存)
      • free之后ptr依然指向原先的内存,但是已经不能再用了,ptr变成了野指针
int main(int argc, char* argv[]){
        printf("请输入要申请的内存个数\n");
        int n;
        scanf("%d", &n);
        int * p = (int*)malloc(n * 4);
        if(p != NULL){
                for(int i = 0; i < n; i++){
                        p[i] = i;
                }
                for(int j = 0; j < n; j++){
                        printf("%d ", p[j]);
                }
                printf("\n");
                free(p);
        } else {
                printf("error");
        }
}
      • void * calloc(size_t nmemb, size_t size);
        • size_t是无符号整型,他是在库头文件中用typedef定义出来的
        • 功能:在内存中,申请nmemb块,每块的大小为size个字节的连续区域
        • 申请的内存中默认的内容为0
        • 返回值:
          • 成功,申请的内存首地址
          • 失败返回NULL
      • void * realloc(void * s, unsigned int newsize);
        • 重新申请内存:在圆形s指向的内存基础上重新申请内存,新的内存的大小为newsize个字节
        • 如果原先内存后面有足够大的空间,就追加,如果后面的内存不够用,则realloc函数会在堆区找一个newsize个字节大小的内存申请,将原先内存中的内存拷贝过来,然后释放原先的内存,最后返回新内存的首地址
        • 如果newsize比原先的内存小,则会释放原先内存的后面的存储空间,只留前面的newsize个字节
      • malloc/calloc/realloc动态申请的内存,只有在free或程序结束的时候才释放
char * p = (char*)calloc(3,10);//申请连续的3块内存,每块10个字节,共30个字节
//使用realloc在p后面追加20个字节
p = (char*)realloc(p, 50)//30+20=50
  • 内存泄露
    • 申请的内存,首地址丢了,找不到了,再也没法使用了,也没法释放了,这块内存就被泄露了
// 案例1
char * p = (char*)malloc(10);//先申请了一块内存
//使用...
p = "hello";//后续,又把p指向了别的地址,这时再也找不到刚才申请的10个字节的动态内存了,也就无法释放了,这10个字节的内存就被泄露了

//案例2
void fn(){
    char * p = (char*)malloc(10);//在函数中申请了一块内存
    //使用...
    //最后没有释放
}
int main(){
    fn();//如果函数中没有主动释放动态内存,则每调一次该函数就会造成一次内存泄露
    fn();
    return 0;
}

结构体

  • 是一种构造数据类型,它是由相同或不同类型的数据构成的集合
  • 使用结构体之前必须先有类型,然后根据类型定义结构体变量
  • 定义结构体类型格式:struct 类型名 { 成员列表 };
  • 定义结构体变量:struct 类型名 变量名;
  • 结构体变量初始化时,必须按成员顺序初始化
  • 结构体变量的使用
    1. 变量名.成员名
  • 结构体变量的大小是 >= 其所有成员之和
  • 相同类型的结构体变量,可以相互赋值
//定义的同时使用,后续还可以使用
struct stu2 {
    char name[20];
    int age;
} lisi;
struct stu2 wangwu;

//无名类型,只能使用一次
struct {
    char name[20];
    int age;
} zhaoliu;

//先定义,后使用
struct stu {
    char name[20];
    int age;
};
struct stu zhangsan = { "aa", 18 };//定义结构体变量,并初始化

//给结构体类型起别名,常在头文件中这样写
typedef struct stu3 {
    char name[20];
    int age;
    char * address;
} STU3;//后续可以直接使用STU3
STU3 xiaoming;
//先定义后赋值
xiaoming.age = 18;
// xiaoming.name = "aa"//错误
strcpy(xiaoming.name, "aa");
// strcpy(xiaoming.address, "bb");//错误
xiaoming.address = "bb";//直接赋值

//结构体的多级引用
struct date {
        int year;
        int month;
};
struct stu {
        char name[20];
        struct date birthday;
};
int main(){
        struct stu boy = { "aa", { 2023, 1 } };
        printf("%d\n", boy.birthday.year);
        return 0;
}

结构体数组

  • 由若干个相同类型的结构体变量构成的集合
  • 定义格式:struct 结构体类型名 数组名[元素个数];
struct stu {
        char name[20];
};
int main(){
        struct stu boys[2] = {
                { "aa" }, { "bb" }
        };
        printf("%s\n", boys[0].name);
        return 0;
}

结构体指针

  • 存放结构体的起始地址的变量,即结构体指针变量
  • 定义格式:struct 结构体类型名 * 指针变量名
  • 结构体变量的地址编号和结构体首个成员的地址编号内容相同,但是指针类型不同
  • 结构体数组的地址就是结构体数组中第0个元素的地址
struct stu {
        char name[20];
};
int main(){
        struct stu boy = { "bb" };
        struct stu * p = &boy;
        printf("%s\n", (*p).name);//使用点访问成员
        printf("%s\n", p->name);//使用箭头访问成员
        return 0;
}

结构体内存分配

  • 规则1:给结构体变量分配内存时,会去结构体中找基本类型的成员,哪个基本类型的成员占到字节数多,就以它的大小为单位开辟内存,double类型的例外
    • char 1字节
    • short int 2字节
    • int float 4字节
    • double 在vc6.0和VS编辑器中以8字节为单位,在Linux的gcc环境中以4字节为单位
    • 指针 4字节
    • 成员中出现数组时,按多个变量处理
    • 内存中存储结构体成员的时候,是按定义时的顺序存储的
  • 规则2:字节对齐
    • char 1字节对齐,即存放char型的变量,内存单位的编号是1的倍数即可
    • short int 2字节对齐,即存放short int型的变量,其实内存单元的编号是2的倍数即可
    • int 4字节对齐,即存放int型的变量,起始内存单元的编号是4的倍数即可
    • long int 在32位平台下4字节对齐,即存放long int型的变量,起始内存单元的编号是4的倍数即可
    • float 4字节对齐,即存放float型的变量,起始内存单元的编号是4的倍数即可
    • double 在vc6.0和VS编辑器中以8字节对齐,在Linux的gcc环境中以4字节对齐
    • 成员中出现数组时,按多个变量处理
    • 开辟内存时,从上向按成员顺序依次开辟空间
  • 为什么要有字节对齐
    • 用空间来换时间,提高cpu读取数据的效率
  • 指定对齐原则
    • 使用#pragma pack改变默认对齐原则
      • 格式:#pragma pack(指定对齐值)
      • 指定对齐值只能是2的n次方,如:1 2 4 8等
      • 指定对齐值和数据类型对齐值相比,取较小值

位段

  • 在结构体中以位为单位的成员,咱们称之为位段/位域
  • 不能对位段成员取地址,因为它可能不够一个字节
  • 一个字节(k)是8位(bit)
  • 32位系统即 32bit 4k,即读取数据时是每次按 4k 为单位来读取的
  • 64位系统即 64bit 8k,即读取数据时是每次按 8k 为单位来读取的
  • 位段成员的类型只能是整型或字符型
  • 一个位段成员必须存放在一个存储单元中,不能夸两个单元,如一个存储单元不能容纳下一个位段,则舍弃剩余空间,跳到下一个存储单元存储
  • 位段存储单元
    • char 型位段 1字节
    • short int 型位段 2字节
    • int 型位段 4字节
    • long int 型位段 4字节
struct Stu {
        unsigned int a:2;//指定a成员只占2位空间
        unsigned int b:6;//指定a成员只占6位空间
        unsigned int c;//int占4字节,即占32位
        unsigned :2;//无意义的位段,表示主动浪费两位空间
};
struct Stu boy;
// 位段段使用
boy.a = 2;// 注意给位段成员赋值时,不能超出其定义时的范围,如a定义是2位,最大值是3(112)
//boy.a = 5;如果给a赋值5,取5的二进制的低两位来赋值,即101

//如果一个位段想要单独从另一个存储单元开始,可以用如下方式
struct Stu2 {
        char a:1;
        char b:2;
        char :0;//这里由于用了长度为0的位段,其作用是:使下个位段从下一个存储单元开始存放
        char c:3;
};

共用体

  • 共用体和结构体类似,也是一种构造类型的数据结构。在进行某些算法的时候,需要使用几种不同类型的变量存到同一段内存单元中,几个变量所使用空间相互重叠。共用体所有成员占有同一段地址空间
  • 定义共用体类型和结构体一样只需改一下关键字即可:union 类型名 { 成员列表 };
  • 定义共用体变量:union 类型名 变量名;
  • 共用体大小是其占内存长度最大的成员的大小
  • 特点
    • 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用
    • 共用体变量中起作用的成员时最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖
    • 共用体变量的地址和它的各成员的地址都是同一地址
    • 共用体初始化时,只能为第一个成员赋值,不能给所有成员都赋初始值

枚举

  • 枚举类型定义:enum 类型名 {枚举值列表};
  • 枚举变量定义:enum 类型名 变量名;
  • 枚举类型的成员都是常量
  • 在枚举类型中,每个枚举常量代表一个整型值,默认从0开始依次编号,定义枚举类型时可以给枚举常量进行初始化值
enum week { mon, tue, wed = 5, thu, fri, sat, sun };//这里给wed初始化为5后,后面的thu将从6开始编号
int main(){
        enum week a = mon;//限定a变量的值只能是枚举类型week中的成员
        enum week b = tue;
        enum week c = wed;
        enum week d = thu;
        printf("%d\n", a);//0
        printf("%d\n", b);//1
        printf("%d\n", c);//5
        printf("%d\n", d);//6
        return 0;
}

文件

  • 文件分类
    • 磁盘文件:指一组相关数据的有序集合,通常存储在外部介质(如磁盘)上,使用时才调入内存
    • 设备文件:在操作系统中把每一个与主机相连的输入、输出设备看作是一个文件,把他们的输入、输出等同于对磁盘文件等读写
      • 键盘:标准输入文件
      • 屏幕:标准输出文件
      • 其他设备:打印机、触摸屏、摄像头、音箱等
    • 在linux操作系统中,每一个外部设备都在/dev目录下对应着一个设备文件,咱们在程序中要想操作设备,就必须对与其对应的/dev下的设备文件进行操作
  • 标准io库函数对磁盘文件的读取特点
    • 内存(程序 数据区) -> 输出(文件缓冲区(系统/程序)) -> 磁盘(文件)
    • 磁盘(文件) -> 输出(文件缓冲区(系统/程序)) -> 内存(程序 数据区)
  • 全缓冲
    • vs中对普通文件的读写是全缓冲的
    • 标准io库函数,往普通文件读写数据的,是全缓冲的
    • 刷新缓冲区的情况
      1. 缓冲区满了,刷新缓冲区
      2. 调用函数刷新缓冲区 fflush(文件指针)
      3. 程序结束会刷新缓冲区

磁盘文件的分类

  • 一个文件通常是磁盘上一段命名的存储区,计算机的存储在物理上是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以字节为单位进行顺序存储
  • 从用户或者操作系统使用的角度上把文件分为两类:
    • 文本文件:基于字符编码的文件
      • 常见编码有:ASCII、UNICODE等
      • 一般可以使用文本编辑器直接打开
      • 如:5678 以ASCII存储的形式为:00110101 00110110 00110111 00111000
    • 二进制文件:基于值编码的文件
      • 基于值比阿妹,根据具体应用,指定某个值是什么意思,一般需要自己判断或使用特定软件分析数据格式。二进制文件以 位 为单位来表示一个意思的
      • 如:5678 的二进制存储形式为:00010110 00101110
      • 音频文件(mp3):二进制文件
      • 图片文件(bmp)文件,一个像素点由两个字节来描述*****#####&&&&& 565
        • *代表红色的值R
        • #代表绿色的值G
        • &代表蓝色的值B
  • 文本文件和二进制文件的区别
    • 译码
      • 文本文件编码基于字符定长,译码容易些
      • 二进制文件编码是变长的,译码难一些(不同的二进制文件格式有不同的编码方式,一般需要特定的软件进行译码)
    • 空间利用率
      • 二进制文件用一个比特来代表一个意思(位操作),而文本文件任何一个意思至少是一个字符,所以二进制文件空间利用率高
    • 可读性
      • 文本文件用通用的记事本工具就几乎可以浏览所有文本文件
      • 二进制文件需要一个具体的文件解码器,比如读BNP文件,必须用读图软件
    • 总结
      1. 文件在硬盘上存储的时候,物理上都是用二进制来存储的
      2. io库函数对文件操作时,不管文件的编码格式(字符编码/二进制),都是按字节对文件进行读写,所以通常管文件又叫流式文件,即把文件看成一个字节流

文件指针

  • 文件指针并不是指向文件地址的指针,因为文件是存放在磁盘上的,并不在内存中,它指向的是文件描述信息结构体的地址
  • 文件指针在程序中用来标识一个文件的,在打开文件的时候得到文件指针,对文件指针的操作就是对文件的操作
  • 定义文件指针的一般形式:
    • FILE * 指针变量表示符;
    • 需要 stdio.h 头文件
    • FILE时系统使用typedef定义出来的有关文件的一种结构体类型,结构中含有文件名、文件状态、文件当前位置等信息
  • 在缓冲文件系统中,每个被使用的文件都要在内存中开辟一块FILE类型的区域,存放与操作文件相关的信息
  • 文件操作过程
    1. 打开文件得到文件指针
    2. 通过文件指针进行读写操作
    3. 操作后,要关闭文件
  • c语言中有三个特殊的文件指针无需定义,可以直接使用
    • stdin:标准输入,默认为当前终端(键盘),使用scanf、getchar函数默认从此终端获得数据
    • stdout:标准输出,默认为当前终端(屏幕),printf、puts函数默认输出信息到此终端
    • stderr:标准错误输出设备文件,默认为当前终端(屏幕),当程序出错时使用 perror 函数将信息打印在此终端

api

  • 需要 stdio.h 头文件
  • stdio.h头文件中定义了一个宏:EOF,值为-1
  • fopen
    • FILE * fopen(const char * path, const char * mode);
    • 打开一个已经存在的文件,并返回这个文件的描述指针,或者创建一个文件,并打开此文件,然后返回文件描述指针
    • 打开失败或创建失败时返回NULL
    • mode 权限
      • r 只读
      • w 只写-文件不存在时,以指定文件名创建此文件并且打开;文件存在时清空文件内容,再打开文件
      • a、a+ 追加-文件不存在则创建并打开(同w);若存在则从文件的结尾处进行写操作
      • +、r+、w+ 可读可写
      • rb、wb 、ab 带b的模式是以二进制形式打开文件
  • fclose
    • int fclose(FILE * fp);
    • 关闭指定的文件,只能关闭一次,不可多次关闭同一个文件,文件关闭后不开再进行读写操作
    • 成功返回0,失败返回非0的数值
  • fgetc
    • int fgetc(FILE * stream);
    • 从stream所标识的文件中读取一个字节,将字节值返回
    • 返回值
      • 以t(文本)的方式:读到文件结尾返回EOF
      • 以b(二进制)的方式:读到文件结尾,使用feof(文件指针)判断结尾
        • feof是c语言标准库函数,其原型在stdio.h中,用于检测流上的文件结束符,如果文件结束,返回非0值,否则返回0
  • fputc
    • int fputc(int c, FILE * stream);
    • 将c的值写到stream所代表的文件中
    • 返回值
      • 成功返回输出的字节值
      • 失败返回一个EOF
  • fgets
    • char * fgets(char * s, int size, FILE * stream);
    • 一次读写一个字符串,碰到换行符或文件末尾则停止读取;或者读取了size-1个字节即停止读取。
    • 在读取的内容后面会加一个 '\0',所谓字符串的结尾
    • 成功返回目的字符数组的首地址
    • 失败返回NULL
  • fputs
    • int fputs(const char * s, FILE * stream);
    • 将指定字符串写到文件中功能
    • 成功返回写入的字节数
    • 失败返回EOF
  • fread
    • size_t fread(void * ptr, size_t size, size_t nmemb, FILE *stream);
    • read函数从stream所标识的文件中读取数据,每块是size个字节,共nmemb块,存放当ptr指向的内存里
    • 将文件中的数据原样读取到内存里
    • 返回实际读到的块数
  • fwrite
    • size_t fwrite(void * ptr, size_t size, size_t nmemb, FILE * stream);
    • 将ptr指向的内存里的数据,向stream标识的文件中写入数据,每块是size个字节,共nmemb块
    • 例:如果ptr是有5个元素的数组,即nmemb应写为5块,size即数组中每个元素的字节数
    • 将内存中的数据原样输出到文件中
    • 返回实际写入的块数
  • 随机读写
    • 实现随机读写到关键是按要求移动位置指针,这称为文件的定位
    • 完成文件定位的函数有:rewind、fseek
  • rewind
    • void rewind(FILE * stream)
    • 把文件内部的位置指针移动到文件首部
    • 文件指针经过写操作后已经到了末尾,使用rewind可将其复位
  • ftell
    • long ftell(文件指针)
    • 获取文件流目前的读写位置
    • 返回当前读写位置距离文件起始的字节数,失败返回-1
  • fseek
    • int fseek(FILE * stream, long offset, int whence);
    • 移动文件流的读写位置
    • 一般用于二进制文件,即打开文件的方式需要带b
    • whence 起始位置,有三个宏
      • 文件开头 SEEK_SET 0
      • 文件当前位置 SEEK_CUR 1
      • 文件末尾 SEEK_END 2
    • offset 位移量
      • 以起始点为基点
      • 正数往文件的末尾方向偏移
      • 负数往文件的开头方向偏移
FILE * fp = fopen("./test.txt", "rb");//以二进制的方式,只读模式打开文件
fseek(fp, 0, SEEK_END);//将位置指针定位到末尾
long len = ftell(fp);//将位置指针定位到末尾后,再通过ftell获取当前位置距离起始位置的字节数,即可得到文件的字节大小
printf("%lu\n", len);
rewind(fp);//复位位置指针
  • fprintf
    • 根据指定格式向文件写入数据
  • fscanf
    • 根据指定格式读取文件数据
FILE *fp = fopen('xxx.txt', "w+");
char c = 'a';
int num = 10;
fprintf(fp, "%c %d\n", c, num);
rewind(fp);
fscanf(fp, "%c %d\n", &c, &num);
fclose(fp);

常见报错信息

  • segmentation fault
    • 段错误
    • 可能是有索引越界的情况
    • 可能是栈被占满了,比如:无限递归
  • Bus error
    • 总线错误
    • 可能是修改常量数据导致的
  • Abort trap
    • 陷阱中止
    • 可能是内存溢出导致的,如:把一个长度较大的字符串拷贝到较小的字符串中
  • Undefined symbols
    • 未定义的符号,连接错误
    • 可能是只声明了函数名,未定义函数体

推荐文章