c语言
0.来源与概念
来源
C语言诞生于1972年的贝尔实验室,由Dennis Ritchie设计。它最初是作为Unix系统的开发工具而发明的,并继承了B语言的许多思想,同时加入了数据类型的概念及其他特性。C语言是一种较早的程序设计语言,但至今仍然广泛应用于各种编程领域,尤其是在底层开发和系统编程方面。
概念
C语言是一门面向过程的、抽象化的通用程序设计语言。它以简易的方式编译、处理低级存储器,并产生少量的机器语言,同时不需要任何运行环境支持便能运行,因此具有高效率的特点。C语言的设计目标是提供一种能以简易方式处理低级存储器、产生少量机器码且运行效率高的编程语言。
C语言广泛应用于底层开发,如操作系统、嵌入式系统、驱动程序等。同时,它也常用于编写系统软件和应用软件。C语言具有强大的数据处理能力,能够处理各种数据类型,包括整型、浮点型、字符型等,并支持指针操作,这使得C语言在内存管理和硬件操作方面具有独特的优势。
1.入门程序
注:一个项目只能有一个主入口main
#include<stdio.h> //预处理 int main() //主程序入口 { printf("你好你好"); //业务代码 return 0; //返回值 }
执行流程
将代码文件demo.c 编译成计算机语言 二进制文件 demo.obj 然后结合预处理中使用的文件 stdio.h 一起连接成为 demo.exe可执行文件
然后运行
2.核心语法
注释
//注释 /*注释*/ //快捷键 ctrl+k,c 注释 ctrl+k,U 取消注释
关键字
/*编程语言中一些关键的语法字 首字母全是小写 具有特定的含义 如数据类型 构造 等等。。*/ int float char long default sizeof goto return ...
常量
//在程序执行的过程中 值不会发生改变的量为常量 //四种字符常量 整数 1 2 %d 实型 1.1 2.3 .84 = 0.84 1.33E7 1. = 1.0 %f 字符 'A' 'b' 'd' //只能有单个数字/字母/英文符号 %c 字符串 "acv" //双引号中的都是字符串 %s //输出占位符 printf(参数1,参数2) //表示的是最终的输出 /* 参数1 最终输出必须为字符串 也可直接输出一个字符串 不要参数二 如果要输出其他类型就必须使用占位符 参数二提供参数 */ printf("%d",12) printf("%s\n","avb"); //\n 换行符 windows \r\n MAC \r linux \n
变量
变量作为一个装数据的容器 对其进行赋值修改
数据类型 变量名
经常发生改变的值为变量
//变量定义 //vs中 变量的定义是任意位置的 //在vc中变量的定义统一在上面定义 int main() { int a; int c = 3; int b; if(c == 3) { a = c; } printf("%d",a); }
数据类型
作用:变量中能存储什么类型的数据
存储空间的大小
整型
short ds =130; long l =100L; long long sd = 1000LL; printf("%ld\n",l); printf("%lld\n",sd); printf("%zu\n",sizeof(ds)); printf("%d\n",sizeof(ds)); sizeof() //用于计算占位多少 可写数据类型 可写参数名 //完整形态 short int ds =130; long int l =100L; long long int sd = 1000LL; //有无符号 signed //有符号 +- unsigned //无符号 +
小数(实型)
float a = 1.1323F; long double c = 3.1412414L; double d = 123.32; //通常小数使用的 double b = 1.231; //保留位数 printf("%.2f\n",a); //保留三位 printf("%.3lf\n",b); printf("%lf",d); //无法使用signed 和 unsigned
字符
char a = 'a' //无法使用 中文符号 中文 取值范围位ascll码表中的数据 //占用1个字节 printf("%c",a)
字符串
没有直接对字符串进行定义符号 使用char 加数组的形式进行定义
char str[] = "aaa"; char str[] = {"aaa"}; //字符串数组 char str[4]; /*括号中的数字代表该字符串的字节大小 英文:一个字母一个字节 中文:一个中文两个字节 最后存在一个隐藏的结束符 占用一个字节 字节大小可以过大 但是不能过小 */
标识符
自己起的名字和函数名
规则:1.由数字 字母 下划线组成
2.数字不能开头
3.不能是关键字
4.区分大小写
键盘录入
scanf(参数1,参数2)进行键盘录入
参数与printf一样
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { int a; int b; int d; scanf("%d",&d); printf("%d",d); //录入多个数据 scanf("%d %d",&a,&b); scanf("%d,%d",&a,&b); } //案例 专升本题目 键盘录入输入长方体 长宽高 求三个面的面积 体积 保留两位小数 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { //使用vc++需要提前将变量定义 否则会报错 //double 为 lf float为 f double l; double w; double h; double area_A; double area_b; double area_c; double volume; printf("输入长宽高:"); scanf("%lf %lf %lf",&l,&w,&h); printf("长:%lf\n",l); printf("高:%lf\n",h); printf("宽:%lf\n",w); area_A = l *w; area_b = h *w; area_c = l *h; volume = l *h*w; printf("A的面积是:%.2lf\n",area_A); printf("b的面积是:%.2lf\n",area_b); printf("c的面积是:%.2lf\n",area_c); printf("长方体的体积是:%.2lf\n",volume); return 0; }
3.算数运算符
基本运算
+ - * / % 加 减 乘 除 取余
细节:整数计算 结果一定是一个整数
小数计算结果一定是一个小数
小数和整数计算结果为小数
小数直接参与计算结果可能会产生偏差
除法不能除0
取余的时候运算的数据类型必须为整数
取余的征服是和第一个数字保持一致的
不能%0
//案例一:获取一个数的各个位 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { int num; int ge; int shi; int bai; printf("输入一个三位数"); scanf("%d",&num); ge = num % 10; shi = (num-ge)/10 % 10; bai = (num - (shi*10 + ge)) /100 % 10; printf("ge: %d\n",ge); printf("shi: %d\n",shi); printf("bai: %d\n",bai); return 0; }
运算转换
在c语言中不同的数据类型需要进行转换数据类型才能进行计算
隐式转换
转换类型的时候自动转换 可以将取值范围小的数据类型转换为取值范围大的数据类型
double >float >long long >long >int >short >char
//此过程进行了隐式转换 在进行数据计算 赋值等操作时会触发 short a = 10; int b = a;
规则:取值范围小的会自动转换成取值范围大的数据类型在进行计算
//该过程中先将int a 转换成了double a 然后再进行运算 int a = 10; double b = 10.3; double c= a+b;
2.char 和 short类型在进行运算的时候都会先提升为int 类型 再进行计算
强制转换
取值范围大的赋值给取值范围小的
注:强制转换的时候数据可能会发生改变
short a = 10; short b = 20; int c = a+ b; short d = (short)(a + b);
字符相加 与java相同使用ascll码表中的进行计算 先转换成int类型 对于的数字然后再进行计算
自增自减运算
单独运算
++a a++ //结果不会变 任然是a自增1 a-- --a //同理
参与计算
int a = 10; int b = a++; //先用后加 b = 10 int c = ++a; //先加后用 c = 11 //a = 11 int i = 10; int j = 5; int k = i++ + ++i - --j - i--; K = 7; //windows:前缀优先于后缀 /* 前缀统一自增/自减 再把结果拿出来用 int i = 10; ++i + ++i; 12 12 = 24 后缀统一先用,等整个表达式结束后再进行自增 int i = 10; i++ + i++ 10 10 = 20 i = 12 */ //linux mac:6 /* 前后缀的优先级一样从左到右依此进行计算 每一个前后缀都是一个独立的个体 int i = 10; i++ + i++ 10 11 ++i + ++i 11 12 */
赋值运算符
= += -= *= /= %=
int a+=b; //a = a+b
关系运算符
== != > < >= <=
== //全等 真为1 假为0 != //不等 > < >= <= //比较符
逻辑运算符
|| && !
|| //或 && //且 ! //非 //在表达式中都会执行左边式子 如果左边式子为1 && 后面的才会继续执行 ||后面不会执行 int a = 1; a > 0 && ++a; //a = 2 a< 0 && ++a; //a = 1
三元运算符
a>b? a : b;
可以进行嵌套
if(a>b) { return a; } else { return b; } //三元运算符嵌套问题 /* 从左边开始找问好 找到一个那么一个冒号与之对于 如果多一个问好那么对应的冒号数量加一 */
运算符优先级
小括号最优先级
取反>算数运算
一元 > 二元 > 三元
&& > || > 赋
4.流程控制语句
if语句
int a = 10; if(a >= 10) { printf("大于等于10\n"); } //括号中大于1 为真 为0的话为假 非零条件成立 if(' ') // 1 -1 字母a~Z 字符'任意'(内不能为空) 字符串 "任意" { printf("也可以打印\n"); } //如果语句体中只有一条语句 大括号可以省略 if(a == b) printf("a大于b"); //else 分支 if(' ') // 1 -1 字母a~Z 字符'任意'(内不能为空) 字符串 "任意" { printf("也可以打印\n"); }else{ printf("这句话是打印不到了"); } //else if 分支 if(a > b) { printf("a") }else if(a < c) { printf("c") }else { printf("b") }
switch语句
//语句结构 /* 表达式的值只能是字符/整数 值:只能是字符或整数字面量 不能是变量 */ switch(表达式){ case 值1: 语句体1; break; case 值2: 语句体2; break; ... default: 语句体; break; } int day; printf("输入今天星期数"); scanf("%d",&day); switch(day) { case 1: printf("跑步"); break; case 2: printf("游泳"); break; case 3: printf("健身"); break; case 4: printf("吃饭"); break; case 5: printf("睡觉"); break; case 6: printf("跑步"); break; case 7: printf("看电影"); break; default: printf("输入错误"); break; } //case穿透 /* 没有break的时候case会继续往下进行穿透直到有break的值出现 例如 1-5没有填写break 6填写了break 那么就会打印跑步游泳健身睡觉吃饭跑步直达break case只会往下穿透 使用场景: 当代码中有多个语句体重复的时候 可以使用case穿透 */ switch(num) { case 1: printf("机票查询"); break; case 2: printf("机票改签"); break; case 3: printf("机票预定"); break; case 4: default: printf("退出"); break; }
for循环
执行效果 执行流程与 java中的一致 知道循环次数
for(int i = 1; i<=10; ++i) { printf("你好"); } //vc中的写法 vs写法可以随意进行定义变量 int i; int num; int count = 0; printf("请输入一个数字\n"); scanf("%d",&num); for(i = 1; i<= num; i++) { if(i % 6 == 0 && i % 8 == 0) { count++; } } printf("%d\n",count);
while循环
不知道循环次数 随时可以跳出循环
int i = 0; while(1) { i++; printf("%d",i); if(i == 10) { break; } } //案例 求算数平方根 while(j*j<=num) { j++; } printf("该数的算数平方根为:%d\n",j-1)
do..while循环
/* 无论如何都会执行do里面的语句 然后再执行条件语句 如果复合条件语句那么继续执行循环直到不符合条件跳出循环 例如:你去吃饭不管兜里有没有钱 先吃一顿再说吃完再看条件 */ do { printf("%d",i); i++; }while(i<=5);
break和continue和goto
//break 直接跳出当前循环 结束循环 int num; int i; int j = 1; scanf("%d",&num); for(i=1;i<=num/2+1;i++) { if(i*i>num) { int result = i - 1; printf("该数的算数平方根为%d\n",result); //找到改数直接跳出循环 break; } } //continue //跳出此次循环 继续下一次循环 //案例:求1-100之间的奇数平方 如果该数被三整除那么跳过 int i = 1; for(i=1;i<=100;i++) { int temp = 0; if(i%2 == 1) { if(i % 3 == 0) { printf("该奇数且能被三整除%d\n",i); //找到该数直接下一次循环 continue; } temp = i * i; printf("该奇数不被三整除的数%d\n",temp); } } /*goto用于指定跳转到代码的位置 一般用于跳出循环*/ int i = 1; a: i++; if(i <= 15) { goto a; } printf("%d",i);
5.函数
与java的方法大致 缺少限定词 public等 都是返回类型 函数名进行定义
/*函数定义 函数要定义在main上面 从上往下进行执行 如果定义在下面 那么在上面需要进行声名 参数类型 函数名() { 函数体 } 如果有参数类型 那么需要添加return 返回对应的数据 如果括号中有参数 形参 那么在调用方法的时候需要将对应的参数添加进去 参数类型 函数名(形参1,形参2 ...) { 函数体 return 返回值; } int sum = playGame(实参1,实参2) */ #include <stdio.h> //定义在下面的函数需要在上面声名 playGame(int num1,int num2); void Game() { printf("打游戏\n"); printf("打游戏\n"); printf("打游戏\n"); printf("打游戏\n"); } int main() { int sum = playGame(10,20); printf("%d",sum); Game(); return 0; } //定义在下面的函数 int playGame(int num1,int num2) { return num1+num2; }
常见函数
#include <stdio.h> //数学库 #include <math.h> //标准库 #include <stdlib.h> //时间库 #include <time.h> int game(int num1,int num2) { return num1+num2; } int main() { long long res; int num; double ce; double flo; //数学库 幂次方 double sum = pow(2,3.0); //绝对值 int ab = abs(32); //向上取整 ce = ceil(23.3); //向下取整 flo = floor(12.3); //获取当前时间 res = time(NULL); //获取随机数 //设置种子 //获取随机数 /* 随机数的弊端 1.种子不变 随机数的结果是固定的 2.随机数的范围 是 0-32767 想要获取任意范围的随机数: 1.把这个范围变成包头不包尾 包左不包右 2.拿着尾巴减去开头 3.修改代码 % 结果 + 开头 求 12 - 87 1. 12 -88 2. 88 -12 = 76 3. %76 + 12 num = rand()%76 + 12; */ srand(23123); num = rand()%76 + 12; printf("%d - %d - %d",num,ce,flo); return 0; }
案例:猜数字小游戏
#include <stdio.h> //标准库 #include <stdlib.h> //时间库 #include <time.h> void gassGame() { int res; int gassNum; srand(time(NULL)); res = rand() % 100 + 1; while(1) { printf("请输入要猜的数字\n"); scanf("%d",&gassNum); if(gassNum > res) { printf("猜大了请重新猜\n"); }else if(gassNum < res) { printf("猜小了请重新猜\n"); }else{ printf("恭喜你猜对了\n"); break; } } } int main() { int gameStart; start: printf("请确认开始游戏 1:开始 0结束\n"); scanf("%d",&gameStart); switch(gameStart) { case 1: gassGame(); printf("恭喜你赢了 是否再次游玩\n"); goto start; default: goto over; } over: printf("游戏退出\n"); return 0; }
6.数组
与java一样可以储存同种数据类型的数据的容器 与鸿蒙不同 鸿蒙可以存储多种数据类型 数据不能添加
(能够自动转换的数据可以存储进去)
定义
特点:数组的长度一旦声名不可已改变
数组是一个连续的空间
//数据类型 数组名[数组长度] int arr[3]; //定义: 数据类型 数组名[数组长度] = {数据,数据..} int arr[3] = {1,2,3} int arr[] = {1,2,3} //如果数组的长度没有定义 那么数据多少就是这个数组的长度 /* 如果定义的数组长度超过填充数据的长度那么多余的数据会自动填补 int 填 0 double 0.0 char '\0' string Null int arr[4] = {1,2} => {1,2,0,0} double arr[4] = {1.1 , 2.2} => {1.1 , 2.2, 0.0, 0.0} char arr[4] = {'a','b'} => {'a','b','\0','\0'} string arr[4] = {"a","a"} => {"a","a",Null,Null} */
元素访问
获取 修改
/* 与java一致 获取:根据索引下标进行获取元素 数组名[索引] 修改与java一致 */ int num = arr[0] arr[1] = 10;
遍历数组
int i; unsigned char len; int age[] = {20,12,32,12,12}; len = sizeof(age) / sizeof(age[0]); for(i = 0;i<len;i++) { printf("%d\n",age[i]); }
c语言中的内存地址&数组的内存地址
/* 内存:用于存储临时的数据 计算机中的内存:32位与64位是十进制位数的差别 内存占用大小也存在差别 每一位每一个字节就是一个内存空间 内存空间:将内存分成多个网格空间每一个空间代表一个内存字节都会分配一个内存地址 每一个变量都会占用内存地址 根据内存地址寻找到储存的变量数据 查找内存地址的时候为首地址 第一个字节占用的地址 int 为四个字节 占用连续的四个内存空间地址 第一个就是首地址 c语言中用%p来表示内存地址占位符 &变量名 表示的是该变量的内存地址 */ int a = 10; printf("%p",&a); scanf("%p",&a); //键盘输入指向的变量是该变量的内存地址
数组中的内存
/* 数组中的内存地址 int arr [] = {1,2,3} 1. int 数据类型占用4个字节 也就是在内存中占用四个字节空间 每种数据类型占用的对应字节数就是占用空间数 2. 数组中每一个数据占用对应数据类型字节数(空间个数 int 4个) 此时一共占用 3x4 = 12个 3. 数组的内存地址表示位 第一个数据占用的第一个字节空间的地址(也是arr[0]的地址) 4. 然后索引每后一个数据就会向后偏移一个单位(偏移量 -> 根据数据类型定义多少 int(4个)) printf("%p\n",&arr); //00F3FA80 printf("%p\n",&arr[0]); //00F3FA80 +4 printf("%p\n",&arr[1]); //00F3FA84 +4 printf("%p\n",&arr[2]); //00F3FA88 比如计算数组的长度 使用sizeof(arr) 计算出数组的整体字节数量 字节大小x数据个数 /sizeof(arr[0]) 其中一个数据的字节大小 len = sizeof(arr) / sizeof(arr[0]) */
int类型数组内存展示图 (如果是long就是八位每八位一个索引 也就是一个偏移量)
数组注意事项
/* 数组作为函数的参数传递时: 实际上传递的是数组的首地址,如果要在函数中对数组进行遍历的话,要将数组的长度一并传递 定义处:arr表示的是完整的数组 函数处:arr只是一个变量 用来记录数组的首地址 */ int main() { int arr[] ={1,2,3,4,5}; printf("%zu",sizeof(arr)); //20 } void printArr(int arr[]) { printf("%zu",sizeof(arr)); //8 }
算法
基本查找:利用基本的循环遍历查找
二分查找:限定有序的数组进行查找
对最小 最大索引定义 然后计算中间索引 使用中间索引对应的数 和要查找的数进行比较
插值查找
分块查找
哈希查找
冒泡排序
选择排序
二维数组
在数组里面存放数组
//定义:数组类型 数组名[数组长度][内数组长度] = {}; int arr[3][5] = { {1,2,3,4,5}, {11,22,23,34,45}, {111,222,333,444,555}, }; //遍历: for(int i = 0;i <3;i++) { for(int j =0;j<5;j++) { printf("%d",arr[i][j]); } printf("\n"); } //定义长度不一样的二维数组 int arr1[] ={1,2,3,4,5}; int arr2[] = {1,2,3,4,56,6,7,8}; int arr3[] = {1,2,3,4,5,6,7,8,9,0,10}; //定义数组存放前面数组 因为数组直接调用是指针 地址值 所以使用指针的格式定义 int* arr[3] = {arr1,arr2,arr3}; int i; int j; //错误遍历 for(i =0;i<3;i++) { /* 此时循环结果为1 1 1 原因:len = 1 进行sizeof计算的时候直接arr是一个整体 但是arr[i] 参与计算从而退化为指向第一个元素的指针 而该元素 也是一个指针 指针也是占用内存空间的 ----- 32位 4字节 64位 8字节 根据计算机位数此时为32位 指针占用4个字节 4/4 = 1 记住:arr[i] 直接是指向数组中的某一个变量 不管这个变量是什么 他所记录的sizeof() 就是这个变量数据类型的大小 */ int len = sizeof(arr[i])/sizeof(int); for(j = 0;j<len;j++) { printf("%d",arr[i][j]); } printf("\n"); } //正确遍历 int len1 = sizeof(arr1)/sizeof(arr1[0]); int len2 = sizeof(arr2)/sizeof(arr2[0]); int len3 = sizeof(arr3)/sizeof(arr3[0]); int lenArr[3] = {len1,len2,len3}; //遍历 for(i =0;i<3;i++) { for(j = 0;j<lenArr[i];j++) { printf("%d",arr[i][j]); } printf("\n"); }
7.指针
指针
定义:指针就是内存地址
指针变量:储存着内存地址的变量
(也可以将指针称为 指针变量)
//指针:也成为指针变量 记录内存中变量的地址 // 利用指针可以进行获取变量中数据/存储数据 int a = 10; int* p1 = &a; //定义一个指针p1指向变量a(指针的数据类型和指向的变量数据类型一致) 数据类型* 变量名表示定义 此时的*表示标记作为一个指针 *p1 = 200; //利用指针将数据储存 用 *变量名 表示定义 此时的*表示解引用运算符 printf("%p\n",p1); //010FF8CC printf("%d\n",*p1); //200 (根据数据类型字节数进行解码) printf("%d\n",a); //200
小细节:指针变量的名字与普通变量名字是一样的 p1 不带*
指针变量的数据类型要和被指向的数据的数据类型一致
指针变量也是会占用内存空间的 x64占用8个字节 x86占用4个字节 (和数据类型无关 和编译器有关)
给指针变量进行赋值的时候不能直接将数值赋值给指针 错误示范: int* p1 = 500
作用
可以操作其他函数中的变量
//只是将数据传递 将num1和num2数据进行赋值 void swap(int num1,int num2) { int temp = num1; num1 = num2; num2 = temp; } //传递指针变量 直接对指针指向的变量进行赋值 交换数据 void swap2(int* p1,int* p2) { int temp = *p1; *p1 = *p2; *p2 = temp; } int main() { int n1 = 10; int n2 = 20; swap(n1,n2); printf("调用交换1:%d %d\n",n1,n2); //10 20 swap2(&n1,&n2); //20 10 printf("调用交换2:%d %d\n",n1,n2); }
有点类似于java中 this的使用一般
//在对象中 构造函数的赋值(get set一致) 其中this指向的变量 与c语言中*指向的变量 有异曲同工之妙 //这其中this与c中的*一般指向a的地址 直接对其老家(空间所在) 进行赋值改变 public test(){ int a; public void test(int a){ this.a = a; } }
小细节:函数中变量的生命周期和函数相关 ,函数销毁了 变量也会随之销毁。此时在其他函数中,就不能通过指针使用该变量了。
如果不想要该函数中的变量被回收,可以在变量前面加上static关键字
//没有使用 int* getVar() { int a= 10; return &a; } //使用了static int* getVarByStatic() { int static a= 10; return &a; } int main() { int* p = getVar(); int* p2 = getVarByStatic(); printf("拖延时间等待销毁\n"); printf("拖延时间等待销毁\n"); printf("拖延时间等待销毁\n"); printf("拖延时间等待销毁\n"); printf("拖延时间等待销毁\n"); printf("拖延时间等待销毁\n"); printf("%d\n",*p); //-2 printf("%d\n",*p2); //10 }
可以使用函数返回多个值
将指针作为参数传递到函数中 直接对指针进行存储数据
void getMaxAndMin(int arr[],int len, int* max,int* min) { for(int i = 0;i<len;i++) { if(arr[i] < max) { *max = arr[i]; }else{ *min = arr[i]; } } } int main() { int arr[] = {1,3,2,3,4,54,12}; int len = sizeof(arr) /sizeof(arr[0]); int max =arr[0]; int min = arr[0]; getMaxAndMin(arr,len,&max,&min); }
可以将返回值和状态一起返回
/* 用函数获取两个数取余数的结果 如果第二个数为0 那么不能获取结果 返回值为什么??? 如果第二个数不为零 那么可以直接获取到结果 返回值 返回结果吗??? 那如何区分 使用指针将结果赋值 利用返回值将结果状态返回 */ int getData(int num1,int num2, int* res) { if(num2 == 0) { return 1; } *res = num1 % num2; return 0; } int main() { int num1 = 10; int num2 = 3; int res = 0; int flag = getData(num1,num2,&res); if(!flag) { printf("两数的余数为:%d",res); //3 } }
指针高级
指针计算
可以对指针进行计算加减乘除 : 对内存地址进行计算 每增加或者减少就会进行步长运算
int main() { /* 指针运算: 步长:指针移动一次走了多少字节 char:1 short:2 int:4 long:4 long long:8 加法:指针往后移动n步 p+1 减法:指针往前移动n步 p-1*/ int a = 10; short b = 2; int* p = &a; short* p2 = &b; printf("%p\n",p); //00F9F97C printf("%p\n",p+1);//00F9F980 printf("%p\n",p+2);//00F9F984 printf("%p\n",p-1);//00F9F978 printf("%p\n",p-2);//00F9F974 printf("----------p2-----------\n"); printf("%p\n",p2); //00F9F970 printf("%p\n",p2+1);//00F9F972 printf("%p\n",p2+2);//00F9F974 printf("%p\n",p2-1);//00F9F96E printf("%p\n",p2-2);//00F9F96C return 0; }
注:指针有意义和无意义的运算
在连续的数组中可以进行指针加减法进行运算得到数组中的数据
指针-指针可以得出两个指针之间的步长
int main() { int arr[] = {1,2,3,4,5,6,7,8,9,10}; //加 int* p = &arr[0]; printf("%d\n",*p); //1 p = p + 1; printf("%d\n",*p); //2 //减 int* p2 = &arr[5]; printf("%d\n",*p2); //6 p2 = p2 -1; printf("%d\n",*p2); //4 return 0; }
void类型指针
没有数据类型的指针
优点:不同的数据类型的指针式不能相互赋值的 void指针可以打破上面的观念
void指针没有任何数据类型 可以接收任意类型的指针记录内存地址
缺点:void类型的指针无法进行获取数据 也不能参与指针的计算
应用场景:可以提升函数的复用性 传递参数时可以任意类型
void swap(void* p1,void* p2,int len) { //将void转换成char类型字节 进行字节交换位置 实现数据的交换 char* pc1 = p1; char* pc2 = p2; char temp = 0; for(int i = 0;i<len;i++) { //将字节一个一个进行交换 temp = *pc1; *pc1=*pc2; *pc2 = temp; pc1++; pc2++; } }
无意义指针
指针加指针指向未知的内存地址
指针*数值 指向未知的内存地址
指针/数据 指向未知的内存地址
都是没有意义的计算
指向不明的指针: 野指针:指针指向的空间未分配
悬空指针:指针指向的空间已分配,但是被释放了
二级指针和多级指针
二级指针:用指针取记录指针的内存地址(指针变量也是有内存空间记录的)
int a = 10; int b = 20; int* p = &a; int** pp = &p; *pp = &b; // p = &b printf("%d\n",*p); //20
数组指针
指向数组的指针
数组指针两种定义格式 但是用法存在区别 int arr[] = {1,2,3,4,5,6,7,8,9,10}; int* p = arr; //参与计算的时候会自动降为第一个数据的的指针 int* pp = &arr; //不管是否参与计算 任然是整个数组的数组指针 计算时 步长= 数据类型*数组长度
注意:使用sizeof进行计算的时候任然时整个数组的指针 不会进行退化
&arr获取地址的时候不会进行退化
//定义长度不一样的二维数组 int arr1[] ={1,2,3,4,5}; int arr2[] = {1,2,3,4,56,6,7,8}; int arr3[] = {1,2,3,4,5,6,7,8,9,0,10}; //定义数组存放前面数组 因为数组直接调用是指针 地址值 所以使用指针的格式定义 int* arr[3] = {arr1,arr2,arr3}; int i; int j; //错误遍历 for(i =0;i<3;i++) { /* 此时循环结果为1 1 1 原因:len = 1 进行sizeof计算的时候直接arr是一个整体 但是arr[i] 参与计算从而退化为指向第一个元素的指针 而该元素 也是一个指针 指针也是占用内存空间的 ----- 32位 4字节 64位 8字节 根据计算机位数此时为32位 指针占用4个字节 4/4 = 1 记住:arr[i] 直接是指向数组中的某一个变量 不管这个变量是什么 他所记录的sizeof() 就是这个变量数据类型的大小 */ int len = sizeof(arr[i])/sizeof(int); for(j = 0;j<len;j++) { printf("%d",arr[i][j]); } printf("\n"); }
二维数组的指针遍历方式
int arr[3][5] = { {1,23,4,5,5}, {12,234,24,15,115}, {13,231,44,54,532} }; int i; //定义二维数组的指针 //数据类型 * 数据名 = arr 此时内部的数据为int类型数组且长度为5 int(*p)[5] =arr // int (*p)[5] = arr; int j; for(i = 0;i<3;i++) { printf("%p\n",p); //表示二维数组整体的指针也是第一个元素的指针(一旦参与运算便是第一个元素的指针) for(j = 0;j<5;j++){ /*此时指针i = 0 表示二维数组中的第一个元素{1,23,4,5,5} 使用*解引[j]添加索引遍历 i = 1 表示二维数组中的第一个元素{12,234,24,15,115} 使用*解引[j]添加索引遍历 i = 2 表示二维数组中的第一个元素{13,231,44,54,532} 使用*解引[j]添加索引遍历 这里直接解引成数组(也就是数组的地址)再利用索引获取数据 */ printf("%d ",(*p)[j]); /* (*p+j)表示解引出来为数组中的第j个元素的地址 然后根据这个地址进行在解引 这里是 获取数组中每一个数据的地址再进行解引 */ printf("%d ",*(*p+j)); } printf("\n"); //此时的指针作为计算 退化指向第一个元素{1,23,4,5,5} //该元素类型为数组(&arr的计算方式)p++计算出步长直接跳到下一个数组的这 p++; } //第二种数组的遍历方式 int arr1[] ={1,2,3,4,5}; int arr2[] = {1,2,3,4,56}; int arr3[] = {1,2,3,4,5}; //定义数组存放前面数组 因为数组直接调用是指针 地址值 所以使用指针的格式定义 int* arr[3] = {arr1,arr2,arr3}; //定义arr数组的指针 int** p = arr; int j; for(i = 0;i<3;i++) { printf("%p\n",p); //表示二维数组整体的指针也是第一个元素的指针(一旦参与运算便是第一个元素的指针) for(j = 0;j<5;j++){ printf("%d ",(*p)[j]); printf("%d ",*(*p+j)); } printf("\n"); p++; }
数组指针和指针数组
函数指针
利用指针指向对应的函数
格式:返回值类型 (*指针名)(形参);
作用:利用函数指针可以动态地调用指针
#include<stdio.h> void method1(); int method2(int num1,int num2); int main() { int sum; //定义函数指针 void (*p1)() = method1; int (*p2)(int,int) = method2; //调用函数指针 p1(); sum = p2(10,20); printf("%d\n",sum); return 0; } void method1() { printf("method1\n"); } int method2(int num1,int num2) { printf("method2\n"); return num1 + num2; }
函数指针数组案例
#include<stdio.h> int add(int num1,int num2); int sub(int num1,int num2); int mul(int num1,int num2); int dev(int num1,int num2); int main() { /* 定义加减乘除四个函数 键盘录入三个数 前两个表示运算后一个表示选择运算的函数 1:加 2:减 3:乘 4:除 */ int num1; int num2; int choose; int res; printf("输入两个运算的数字:"); scanf_s("%d,%d",&num1,&num2); printf("输入一个选择运算的数字:"); scanf_s("%d",&choose); //定义一个函数指针数组 int (*arr[4])(int,int) = {add,sub,mul,dev}; //调用函数指针数组中的函数 int res = (arr[choose-1])(num1,num2); printf("结果为: %d" ,res); return 0; } int add(int num1,int num2) { return num1 + num2; } int sub(int num1,int num2) { return num1 - num2; } int mul(int num1,int num2) { return num1 * num2; } int dev(int num1,int num2) { return num1 / num2; }
8.字符串
定义
字符串不能直接使用数据类型进行定义 需要转化成字符数组的格式定义
java中直接定义是使用包装类String进行 其底层依旧是字符数组 jdk1.8之前为char类型 1.9之后为byte类型
#include<stdio.h> int main() { /*字符串的定义 1:利用字符数组的格式定义 2:使用指针+“”的格式定义 */ //1.字符数组 char str1[4] = "abc"; char str2[4] = {'a','b','c','\0'}; printf("%s\n",str1); //abc /* 细节: 1.str1的定义格式会在底层自动转化为str2的这种字符数组的格式,且在最后一位使用‘\0’作为结束符 2.数组的长度要么不写 要么多预留一位用来储存'\0' 3.这种字符串的定义方式 是可以对字符串中的内容进行修改的 char str1[0] = ‘A’; Abc */ //2.指针+“”定义 char* str3 = "abcd"; char* str4 = "abcd"; /* 细节: 1.会在底层自动转化为str2的这种字符数组的格式,且在最后一位使用‘\0’作为结束符 2.指针+“”定义的定义格式,会把底层的字符数组放在只读常量区 只读常量区: 1.内容不可以进行修改 2.里面定义的字符串是可以复用的 ---> 如果再次定义一个相同的字符串会在常量区查找 如果相同会把 先定义的字符串地址赋给后创建的 */ printf("%p\n",str3); // 00305740 printf("%p\n",str4); // 00305740 return 0; }
使用键盘录入字符串
//键盘录入字符串并打印 /* 在使用键盘录入字符串的时候不能使用指针的方式定义 因为指针储存的为常量区不可以进行改变 */ char str[100]; char* p; printf("键盘输入一个字符串\n"); scanf("%s",str); printf("%s\n",str); p = str; //遍历 while(1) { if(*p == '\0') { break; } printf("%c\n",*p); p++; }
字符串数组
// 定义一个字符串数组 存储五个学生的名字并遍历 char name1[]="张三"; char name2[]="李四"; char name3[]="王五"; char str[3][100] ={ "zhangsan", "lisi", "wangwu", }; int i; int j; char* nameArr[3] = {name1,name2,name3}; char** p = nameArr; printf("%p\n",p); printf("%p\n",*p); for(i =0;i<3;i++) { printf("%s\n",nameArr[i]); } for(i = 0;i<3;i++) { //遍历每个字符串 while(1) { //解引用数组 在解引用字符串获取每个字符 if(**p == '\0') { printf("\n"); break; } printf("%c",**p); (*p)++; } p++; }
常用函数
都在<string.h>中
strlen
获取字符串长度
//字符串函数 char* str1 = "abc"; char str2[100] = "abc"; char str3[5] ={'a','b','c','d','\0'}; //strlen /*获取到字符串的长度 但是不会获取标识符 在windows中一个中文占用两个字节 也就是两个长度 */ printf("%d\n",strlen(str1)); //3 printf("%d\n",strlen(str2)); //3 printf("%d\n",strlen(str3)); //4
strcpy
复制字符串
//strcpy /* 字符串复制 将第二个字符串的全部内容拷贝到第一个字符串里面 把第一个字符串进行覆盖了 前提:1.第一个字符串是可以被修改的 2.第一个字符串的空间是能够容纳第二个字符串的 */ char* str1 = "abc"; char str2[100] = "abc"; char str3[5] ={'a','b','c','d','\0'}; strcpy(str2,str3); printf("%s\n",str2); //abcd printf("%s\n",str3); //abcd
strcat
字符串合并
//strcat /*字符串拼接 将第二个字符串的全部内容拷贝到第一个字符串的末尾 前提:1.第一个字符串是能够修改的 2.第一个字符串所剩余的空间是能够容纳第二个字符串的 */ char* str1 = "abc"; char str2[100] = "abc"; char str3[5] ={'a','b','c','d','\0'}; strcat(str2,str3); printf("%s\n",str2); //abcabcd printf("%s\n",str3); //abcd
strcmp
字符串比较
//strcmp /* 将字符串进行比较:比较每个字符串的字符和顺序是否一致 一致返回0 不一致 -1 */ char* str1 = "abc"; char str2[100] = "abcd"; char str3[5] ={'a','b','c','d','\0'}; printf("%d\n",strcmp(str2,str3)); //0 printf("%d\n",strcmp(str1,str2)); //-1
strlwr
字符串全转小写
//strlwr _strlwr(新写法) //字符串转小写 英文的大小写 且该字符串是能被修改的 char str2[100] = "abcd"; strlwr(str2); printf("%s\n",str2); //abcd
strupr
字符串全转大写
//strupr _strupr(新写法) //字符串转大写 英文的大小写 且该字符串是能被修改的 char str2[100] = "abcd"; strupr(str2); printf("%s\n",str2); //ABCD
9.结构体
定义
结构体可以理解为 自定义的数据类型
由一批数据组合而成的数据类型
其中里面的每一个数据都是一个结构成员
与java的class有点像
#include<stdio.h> #include<string.h> //可以定义在不同位置 :如果在函数中只能在该函数内使用 如果函数外任何函数可以使用 struct Books { char name[100]; int price; double size; }; int main() { //声名结构体 struct Books book1; book1.price = 30; book1.size = 20.1; //对结构体中的字符串进行赋值的时候不能 直接使用双引号进行赋值 strcpy(book1.name,"小王子"); printf("%s\n",book1.name); printf("%d\n",book1.price); printf("%lf\n",book1.size); return 0; }
将定义的结构体 存放到数组中并循环遍历
struct student { char name[100]; int age; }; struct student stu1 = {"zhangsan",12}; struct student stu2 = {"lisi",14}; struct student stu3 = {"wangwu",13}; int i; //在vc中需要使用指针的格式存放 vs中可以直接进行存放 struct student* stu[3] = {&stu1,&stu2,&stu3}; for(i =0;i<3;i++) { struct student temp = *stu[i]; printf("%s,%d\n",temp.name,temp.age); }
起别名
使用typedef声名 在结构体最后起别名
在调用的时候可以直接使用别名 可以省略struct
typedef struct student { char name[100]; int age; }st; st stu1 = {"zhangsan",12}; st stu2 = {"lisi",14}; st stu3 = {"wangwu",13}; int i; st* stu[3] = {&stu1,&stu2,&stu3}; for(i =0;i<3;i++) { st temp = *stu[i]; printf("%s,%d\n",temp.name,temp.age); }
结构体作为函数参数传递
1.直接作为函数的参数传递 与变量作为参数传递一般 调用完函数对结构体中的数据没有任何影响
#include<stdio.h> typedef struct Student { char name[100]; int age; }S; void method(S st); int main() { S stu = {"aaa",0}; printf("main函数中的数据为:%s,%d\n",stu.name,stu.age); //aaa 0 method(stu); printf("经过method函数中的数据为:%s,%d\n",stu.name,stu.age); //aaa 0 return 0; } /*细节:此时结构体作为参数传递到method中 会自动创建一个新的结构体st 然后将main函数传递的stu中的数据赋值给st 此时修改的数据是st中的数据 主函数中的stu中的数据是不会发生改变的 */ void method(S st) { printf("main函数中的数据为:%s,%d\n",st.name,st.age); //aaa 0 printf("输入修改的学生姓名\n"); scanf("%s",st.name); printf("输入修改的学生年龄\n"); scanf("%d",&(st.age)); printf("main函数中的数据为:%s,%d\n",st.name,st.age); // zhangsan 12 }
2.使用指针的形式作为参数传递 与指针传递一般 会直接对该变量数据进行改变
#include<stdio.h> typedef struct Student { char name[100]; int age; }S; void method2(S* p); int main() { S stu = {"aaa",0}; printf("main函数中的数据为:%s,%d\n",stu.name,stu.age); //aaa 0 method2(&stu); printf("经过method函数中的数据为:%s,%d\n",stu.name,stu.age); //zhangsan 12 return 0; } /*细节:此时将stu的指针传递过来进行修改结构体中的数据 p此时就是stu的指针 通过指针就可以进行修改数据 */ void method2(S* p) { printf("main函数中的数据为:%s,%d\n",(*p).name,(*p).age); //aaa 0 printf("输入修改的学生姓名\n"); scanf("%s",(*p).name); printf("输入修改的学生年龄\n"); scanf("%d",&((*p).age)); printf("main函数中的数据为:%s,%d\n",(*p).name,(*p).age); // zhangsan 12 }
结构体嵌套
#include<stdio.h> #include<string.h> struct Message { char phone[13]; char email[20]; }; struct Student { char name[100]; int age; struct Message msg; }; int main() { struct Student stu; strcpy(stu.name,"zhangsan"); stu.age = 12; strcpy(stu.msg.phone,"131123123123"); strcpy(stu.msg.email,"zhangsan@qq.com"); printf("该学生姓名为:%s\n年龄:%d\n联系电话:%s\n联系邮箱:%s\n",stu.name,stu.age,stu.msg.phone,stu.msg.email); //初始化赋值 struct Student stu2 = {"lisi",12,{"132132123123","lisi@qq.com"}}; printf("该学生姓名为:%s\n年龄:%d\n联系电话:%s\n联系邮箱:%s\n",stu2.name,stu2.age,stu2.msg.phone,stu2.msg.email); return 0; }
结构体案例
#include<stdio.h> #include<stdlib.h> #include<time.h> //1.定义结构体记录景点和票数 struct spot { char name[10]; int vote; }; int main() { /*集体旅游有A B C D 四个景点,使用随机数进行模拟投票共80票 谁的票多 去哪个景点 如果一样 那么 A>B>C>D */ //2.将其放入到数组中 依据优先级顺序进行定义 struct spot arr[4] = {{"A",0},{"B",0},{"C",0},{"D",0}}; //3.循环80次投票 循环0-4依此标识每个景点 int i; int max; srand(time(NULL)); for(i = 0;i<80;i++) { int choose; //此时choose表是随机的景点代表 0a 1b 2c 3d //也表示了景点数组中的索引 可以直接根据索引匹配投票 choose = rand() % 4; //0 1 2 3 arr[choose].vote++; } //遍历查看票数 for(i = 0;i<4;i++) { printf("%s %d\n",arr[i].name,arr[i].vote); } //4.寻找里面最大的景点 max = arr[0].vote; for(i = 0;i<4;i++) { if(arr[i].vote > max) { max=arr[i].vote; } } //5.对最大的景点进行筛选 如果票数相同那么根据优先级选择 此时的优先级为 A>B>C>D正好为数组的顺序 for(i = 0;i<4;i++) { if(arr[i].vote == max) { printf("获取投票数最多的是:%s 票数为:%d\n",arr[i].name,arr[i].vote); } } return 0; }
内存对齐
普通变量内存对齐
不管是结构体还是普通的变量都存在内存对齐
规则:只能存放在自己数据类型的内存倍数上
简单理解:内存地址 / 占用字节 = 结果为整数
例子:int 存放的位置:内存地址一定能被4整除
long long存放的位置:内存地址一定能被8整除
double 存放的位置:内存地址一定能被8整除
int a = 10; int b = 10; printf("%d",&a); //15990080 / 4 = 3,997,520 printf("%d",&a); //15989712 / 4 = 3,997,428
结构体的内存对齐
结构体在变量的内存对齐的基础上多了一条,结构体的总大小 是最大数据类型的整数倍(用来确定最后一个数据补位的情况)
切记: 对齐的时候会补空白字节,但是不会改变原本字节的大小 char补位之后 其本身还是1个字节
心得:我们会把小的数据类型 写在最上面,大的数据类型,写在最下面(节约空间)
struct spot { //内存从0开始计算 内存地址/占用大小 = 整数 double d; // 0 1 2 3 4 5 6 7 char c; // 8 char c2; // 9 10 11 int vote; //12 13 14 15 }; //将小的数据类型放在前面能够填补较少的空 节约内存空间 struct spot2 { //内存从0开始计算 内存地址/占用大小 = 整数 double d; // 0 1 2 3 4 5 6 7 char c; // 8 9 10 11 int vote; // 12 13 14 15 char c2; //16 }; int main() { struct spot spot; //结构体最终大小是最大数据类型的倍数 8*3 = 24 printf("%d",sizeof(spot2)); //24 printf("%d",sizeof(spot)); //16 }
总结
普通变量的内存对齐保持着 内存地址/占用大小 = 整数的规则 不同的数据类型由于占用的大小不同 应当注意
结构体中的变量内存对齐:其计算内存时从上往下进行计算的 将小的数据类型放到上面 更节约内存空间
计算方式
struct spot { //内存从0开始计算 内存地址/占用大小 = 整数 char c; // 0 char c2; // 1 2 3 int vote; //4 5 6 7 double d; //8 9 10 11 12 13 14 15 };
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
c | c2 | vote | vote | vote | vote | d | d | d | d | d | d | d | d |
此时共占用16字节是八的整数倍数 不用进行填补
struct spot2 { //内存从0开始计算 内存地址/占用大小 = 整数 double d; // 0 1 2 3 4 5 6 7 char c; // 8 9 10 11 int vote; // 12 13 14 15 char c2; //16 };
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
d | d | d | d | d | d | d | d | c | vote | vote | vote | vote | c2 |
此时共占用17字节不是八的整数倍 继续填空到补齐24位为止
10.共同体/联合体
定义
可以依此性存储多种数据类型 类似于泛型的概念
union定义
#include<stdio.h> //共用体定义 //也可以使用 typedef的格式定义别名 union Money { char moneystr[100]; int moneyi; double moneyd; }; int main() { //共用体调用 union Money m; //赋值 一次只能对一个数据类型进行赋值 m.moneyi = 1000; return 0; }
特点
共用体的所有的变量都会使用同一个内存空间
每次只能给一个变量进行赋值,因为第二次赋值就会覆盖原有的数据 所以怎么存入的就怎么取出
共用体占用的内存大小 = 最大成员的长度(也受内存对齐的影响)
细节:以最大的单个成员长度为准
总大小一定时最大单个成员的整数倍
//该共用体 单个最大成员长度为 100 //最大单个成员大小为 8 //所以该共用体的大小为 104 会自动补4个字节 104/ 8 = 13 union Money { char moneystr[100]; int moneyi; double moneyd; };
共用体和结构体区别
存储方式:结构体各用各的 共用体:共用一个内存地址只能获取一个数据
内存占用:结构体 各个数据类型大小的整合 共用体:最大数据类型大小
11.动态内存分配
直接向内存空间申请内存地址分配 可以自定义内存空间大小
常用函数
malloc
向内存地址申请连续的空间 参数 (内存空间大小)
#include<stdio.h> #include<stdlib.h> int main() { /*动态内存分配:去内存地址申请一篇可以自定义的空间 可以对该空间进行修改 赋值 常用函数 定义一个可以容纳10个int类型的内存空间*/ //malloc 申请内存空间大小 掌握 int* p = (int*) malloc(10*sizeof(int)); printf("%p",p); //给内存空间赋值 for(int i = 0;i<10;i++) { //第一种赋值方式 *(p+i) = i+1; //第二总赋值方式 底层为 *(p+i) 所以写成 i[p]也没问题 p[i] = i+1; } return 0; }
细节:1.创建的空间单位是字节
2.返回值的类型是void* 没有步长的概念,也无法获取空间中的数据,需要进行强转(通用性更高 所有数据类型可以使用)
3.返回的只是首地址,没有总大小,最好定义一个变量记录大小(存放的数据个数 -> 总大小/数据类型大小)
4.申请的内存空间不会自动消失,如果得不到正确释放,会导致内存泄漏
5.申请的空间过多,会产生虚拟内存
6.申请的空间没有初始值,需要赋值后才能使用
calloc
向内存地址申请连续的空间 参数 (内存空间个数,每个多大) 会自动填入数据 0
#include<stdio.h> #include<stdlib.h> int main() { /*动态内存分配:去内存地址申请一篇可以自定义的空间 可以对该空间进行修改 赋值 常用函数 定义一个可以容纳10个int类型的内存空间*/ //calloc 申请内存空间+初始化数据 自动赋值0 了解 int* pp = (int*)calloc(10,sizeof(int)); for(int i = 0;i<10;i++) { printf("%d",*(p+i)); //0 } return 0; }
细节:在malloc的基础上多了个数据初始化的过程
realloc
realloc 修改内存空间大小 了解 修改后之前内存空间的数据会赋值进去
//malloc 申请内存空间大小 掌握 int* p = (int*) malloc(10*sizeof(int)); printf("%p",p); //给内存空间赋值 for(int i = 0;i<10;i++) { //第一种赋值方式 *(p+i) = i+1; //第二总赋值方式 底层为 *(p+i) 所以写成 i[p]也没问题 p[i] = i+1; } //realloc 修改内存空间大小 了解 修改后之前内存空间的数据会赋值进去 int* p = (int*) realloc((void*)p,20*sizeof(int)); for(int i = 0;i<10;i++) { printf("%d",*(p+i)); //1 2 3 4 5 6.. }
细节: 在调用realloc的时候,修改内存空间大小,原来的内存地址可能会发生改变 因为在修改内存空间大小的时候 如果后面还有空间会接着原内存地址后面扩容,如果后面没有空间 会重新创建一个新的空间 再把数据赋值进去 原来的空间就会free释放
原本的数据是不会丢失的
在修改空间之后 原来的空间会释放掉,函数底层会处理 只需要释放新的的内存空间就行
free
释放内存空间 每次用完要释放
#include<stdio.h> #include<stdlib.h> int main() { /*动态内存分配:去内存地址申请一篇可以自定义的空间 可以对该空间进行修改 赋值 常用函数 定义一个可以容纳10个int类型的内存空间*/ //malloc 申请内存空间大小 掌握 int* p = (int*) malloc(10*sizeof(int)); printf("%p",p); //给内存空间赋值 for(int i = 0;i<10;i++) { //第一种赋值方式 *(p+i) = i+1; //第二总赋值方式 底层为 *(p+i) 所以写成 i[p]也没问题 p[i] = i+1; } //calloc 申请内存空间+初始化数据 自动赋值0 了解 int* pp = (int*)calloc(10,sizeof(int)); for(int i = 0;i<10;i++) { printf("%d",*(p+i)); //0 } //realloc 修改内存空间大小 了解 修改后之前内存空间的数据会赋值进去 int* p = (int*) realloc((void*)p,20*sizeof(int)); //free 释放内存空间 free(p); free(pp); return 0; }
细节:在调用free释放完内存空间之后,空间中的数据叫脏数据 ,可能被清空,可能被修改为其他的,如果还是原来的数据可能是操作系统没来的及删除
c语言中的内存结构
12.文件
路径
文件在电脑中的位置
绝对路径:从盘符出发寻找文件,也是从根路径出发
相对路径:从当前所在文件夹出发 寻找目标文件
转义字符
\ :转义字符 可以改变后面字符的原本含义
path = “\\”
文件读写
fopen("aaa\a.txt","r")
打开文件 参数1设置文件路径 参数2:设置读写模式
读取模式
可以在其基础上添加+号 可以同时兼顾读写功能
文件读取
fgetc
读取字符
#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> int main() { FILE* file; int c; int d; //fopen 参数:1.文件名 为指针定义的常量字符串 2.读取模式 返回值:为FILE类型指针 file = fopen("C:\\Users\\Lifl\\Desktop\\aa.txt","r"); //文件读取流 fgetc 读取一个字符,如果没有读取到 返回-1 c = fgetc(file); //a printf("%c",c); //循环读取 EOF定义的为-1 while((d = fgetc(file)) != EOF) { printf("%c",d); //b c d } //关闭读写文件 fclose(file); return 0; }
fgets
读取字符串
void readStr() { FILE* file = fopen("C:\\Users\\Lifl\\Desktop\\aa.txt","r"); //fgets:读取字符串,读取到换行符结束 如果读不到数据 返回null char arr[1024]; //三个参数 1.用于接收读取字符串的数组,2.最大读取的长度,3.读取的文件 char* str = fgets(arr,1024,file); printf("%s",arr); //直接打印arr printf("%s",str); //赋值到str中也可以 //循环读取 char * str2; while((str2 = fgets(arr,1024,file)) != NULL) { printf("%s",str2); } fclose(file); }
fread
一次性读取多个字符
void FileRead() { FILE* file = fopen("C:\\Users\\Lifl\\Desktop\\aa.txt","r"); //fread 一次性读取多个字节 //四个参数:1.用于装读取数据的数组 2.读取的字节大小 3.读取的个数(数组长度) 4.文件 char arr[4]; //返回值为当前读取的数量 如果没有数据那么返回0 //fread细节:在每次读取的时候 尽量把数组给装满 返回读取的有效字节个数 /* 假设文件100个字节: 数组30个字节: 第一次读取30个字节,把数组装满,返回30 第二次读取30个字节,把数组装满,返回30 第三次读取30个字节,把数组装满,返回30 第四次读取10个字节,把数据放到数组中,返回10 第五次读取0个字节,返回0 */ int n; while((n = fread(arr,1,4,file)) != 0) { /*此时一次读取四个字节 windows64位中文一个占用2个字节 如果文件中为:abc你好你好 那么直接打印arr会出现乱码 3个字节abc加上半个字节你*/ printf("%s",arr); //避免这种状况可以添加for循环打印arr[i] 会在控制台中自动拼接 for(int i;i<n;i++) { printf("%c",arr[i]); } } fclose(file); }
文件写入
fputc:写入一个字符
fputs:写入一个字符串
fwrite:写入多个
void WriteFile() { //写入文件 FILE* file = fopen("C:\\Users\\Lifl\\Desktop\\aa.txt","w"); //fputc 一次性写入一个字节 返回写入的字符数 int n = fputc(97,file); //fputs 一次性写入一个字符串 返回写入成功非负数,一般忽略,写入失败会有一个EOF的错误 int n2 = fputs("abc你好你好",file); //fwrite 一次性写入多个字节 返回写入的字符个数 //参数: 1.写入字符串 2.写入的字符数 3.写入的个数 4.文件 char arr[] = {97,98,99,100,101}; int n3 = fwrite(arr,1,5,file); fclose(file); }
案例:文件拷贝
#include<stdio.h> int main() { /* 文件拷贝: 可读文件:可以使用记事本打开,并且能够读得懂的文件 不可读(二进制文件):使用记事本打开是乱码的文件 拷贝图片 C:\\Users\\Lifl\\Videos\\Captures\\aaa.png 目的地: C:\\Users\\Lifl\\Desktop\\aaa\\copy.png */ //源文件 FILE* file1 = fopen("C:\\Users\\Lifl\\Videos\\Captures\\aaa.png","rb"); //目的地文件 FILE* file2 = fopen("C:\\Users\\Lifl\\Desktop\\aaa\\copy.jpg","wb"); char arr[1024]; int n; while((n = fread(arr,1,1024,file1)) !=0 ) { printf("%d",n); fwrite(arr,1,n,file2); } return 0; }
13.扩展*
枚举
是C语言(以及许多其他编程语言)中的一种用户定义的类型,它允许程序员为整数值指定更易读的名字。枚举类型通常用于表示一组固定的常量值,比如星期中的天数、月份、错误代码等。
enum Day { Sunday = 1, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; enum Day today; //注意,如果显式地为一个枚举常量指定了值,那么它之后的枚举常量的值将基于这个指定的值递增(除非再次显式指定) Day today = Monday; printf("Today is %d\n", today);
位运算
是在整数的二进制表示上直接进行的操作。这些操作允许程序员对存储在变量中的位的各个部分进行单独的操作,如设置、清除、翻转位等。
&(按位与):只有当两个相应的位都为1时,结果位才为1。
|(按位或):只要两个相应的位中有一个为1,结果位就为1。
^(按位异或):当两个相应的位不相同时,结果位为1。
~(按位取反):将数的各个二进制位全部取反,即0变1,1变0。
<<(左移):将数的各二进制位全部左移若干位,左边超出的位丢弃,右边补0。
>>(右移):将数的各二进制位全部右移若干位,右边超出的位丢弃。对于无符号数,左边补0;对于有符号数,某些编译器/平台会使用符号扩展,即左边补符号位。
应用
位运算在底层编程、性能优化、硬件操作、加密算法等领域有着广泛的应用。例如,你可以使用位运算来:
1.检查一个数是否为2的幂(n & (n - 1) == 0
)。
2.交换两个变量的值而不使用临时变量(利用异或运算)。
3.实现对集合的操作(如并集、交集、差集),其中每个集合的元素都是整数,而集合的表示则是一个整数,其中的每个位代表集合中的一个元素是否存在。
4.快速计算一个数的绝对值(针对有符号整数,仅限于某些特殊情况)。
位运算提供了一种强大的工具,用于直接在底层操作数据,但也需要谨慎使用,以避免引入难以发现的错误。