C基础
基础
1、变量类型
char 字符——%c
short 短整——%d
int 整型——%d
long 长整型——%d
long long 更长的整型——%d
float 单精度浮点——%f
double 双精度浮点——%lf
使用
//声明变量,可以引用其他工程内文件的全局变量
extern int val;//需要名字、类型和其他文件的相同
int a = 0; //全局变量,范围是整个工程
int main(){
int b = 0;//局部变量,与全局变量名字冲突时,局部优先
类型 变量名 = 值;//创建变量时最好赋一个值
printf("%d", sizeof(变量名) );
sizeof()//返回的是变量长度,单位是字节byte=8bit,比如char=1,shor=2,int=4;
}
常量
//define定义标识符常量,一般用全大写
#define MAX 100
//3、枚举常量
enum Sex
{
MALE = 3,//给枚举值,赋初值后,打印MALE就会输出3,后面的元素依次递增
FEMALE
};
int main(){
//1、常量是不可变的
const int num = 10;
//2、创建数组时,长度必须是常量来指定,const修饰的是常变量,本质是变量,不等于常量
int arr[纯数字] = {0};
//3、枚举常量使用
enum Sex s = MALE;//规定使用Sex类型的枚举时,只能使用定义的几种值
printf("%d",MALE);//打印结果是0,指的是枚举值第一个
}
字符串
注意项:字符串结束标识是 \0,不会展示在页面上。
char arr[] = "abc";//字符串即字符数组,\0算作一个位置,“abc”实际有4个元素
char arr1[] = {'a', 'b', 'c'};//这种方法创建时,后面不会有\0这个元素,会导致打印出其它乱码字符
strlen(arr);//这个函数返回的结果不包括\0
%s 是字符串占位符
%c 是字符占位符
转义字符
printf("\t");// \+字母代表一个转义字符,打印出来\t是tab制表符
"\n" 换行
"\\" \
//更多转义百度
/*多行
注释*/
2、操作符
简介:
+ - * / %
float a = 9 / 2; //9和2都是整数,执行的是整数除法,必须使其中一个为小数形式才行,比如9.0或2.0
% //取余数
>> << // 左右移操作符,以二进制为基础移动
int b = 2 << 1; //0...00010 ,向左移动一位——> 0...00100变成4,尾部补0,右移反之一样
& | ^ //按位与、或、异或
!//取反,比如0代表false,非0代表true,!0就代表真,!1就代表假
~ //按位取反,将某个数的二进制为,全部反转(包括符号位)
~0 = -1; // 00...0000 ——> 11...1111,二进制首位是符号位,1代表负,0代表正
-1 // 10...001 原码
// 11...110 反码(转成反码,符号位不变)
// 11...111 补码(反码+1),内存存的是补码
0的三码二进制全是0,补码取反后都是1,再按上面的方式转成原码就是-1
正整数,三码相同
详细:
% 取模时,两端必须是整数,不可对0取模
>> 算数右移,左边补原符号,逻辑右移,左边补0
//都要是整数
& 按位与,都是1的时候取1,否则取0
| 按位或,只要有一个是1就取1,否 则取0
^ 按位异或,不同取1,相同取0
3、循环、选择、条件判断符号
注意:许多函数返回值可以用来做判断true和false,0代表false,非0代表true,在写判断循环时,可以将某些函数直接用作循环的判断
条件判断符号
>= <= ==
&& 且,|| 或
三目表达式
exp1 ? exp2 : exp3
真取exp2,假取exp3
,逗号表达式
int d = (a = b+2, c = a+2, b = c+2); //从左至右,依次计算,最后b的值赋值给d
-> //结构体指针->结构体成员
struct Stu *ps = &s;
printf("%s %d",ps->name,ps->age);
if 选择
if (){
xxx;
}
else if(){
xxx;
}
else{}
(18 <= age < 20); //这种判断写法错误,当age是21时,18<=age其实就是1,1<20又是1,会执行后面的代码。
(18 <= age && age < 20); //这是正确写法
while、do while循环
while(){
xxx;
}
do {
xxx;//do会提前执行一次,再进循环
i++;
}while (i <= 10);
break; //跳出循环
continue; //跳出当前循环进入下一次判断
switch 选择
int num = 0;
scanf("%d", &num);
switch(num)// 括号内的表达式必须是整型、字符
{
case 1:
xxx;
break;//记得加break,不然会穿透下去执行
case 2:
xxx;
default ://始终会最后执行的一条
xxx;
}
for 循环
int i = 0;
for (i = 1; i <= 10; i++){ //int i = 1;这个写法需要gc99编译器等支持
xxx;
}
for(; ;){} // 死循环写法
for (i = 1, y = 1; i <= 10 && y <= 10; i++,y++){ //多变量写法
}
4、关键字
auto 自动带入的,创建变量时就有,自动创建、自动销毁的意思,省略
extern 声明引用外部符号
register 寄存器关键字,计算机中数据存储位置有内存、硬盘、高速缓存、寄存器
// register int num = 100; 建议num的值存放在寄存器,并不是必定,寄存器读取速度更快,编译器会自动识别将某些变量自动放入寄存器
signed 有符号
unsigned 无符号,比如数字的正负
static 静态修饰
struct 结构体
typedef 类型定义
union 联合体
volatile
define和include等不是关键字,是预处理指令
typedef,unsigned
typedef unsigned int u_int;
int main(){
u_int num = 100; //通过typedef定义一个类型自己想组合起来的类型,用的时候就直接调用
}
static
1、static修饰局部变量
void test(){
static int a = 1;
a++;
printf("%d",a);
}
int main(){
test();//如果a没有static,每次调用都是输出2
test();//如果a被static修饰,第一次test函数结束后,a没有被销毁,a就变成了2,这次输出的结果是3
}
static修饰局部变量改变了变量的存储类型,内存分为堆、栈、静态区
栈:局部变量、函数的参数
堆:动态内存分配的
静态区:全局变量,static修饰的变量
没有static,test结束后a就销毁了,下次进入又是一个新的a
有static,test结束后a还在,并且值已被++
2、static修饰全局变量
另一份文件:static int g_val = 100;
extern int g_val;//声明外部符号
int main(){
printf("%d", g_val);//运行报错
}
static修饰全局变量后,除了原文件内可以使用,其它文件不能使用
3、static修饰函数
extern int Add(int x, int y); //声明外部函数
int main(){
int sum = Add(1,2); //没被static修饰时正常调用
//当原文件的Add()被static修饰后,就无法调用了
}
static修饰函数后,只能在原文件内使用
define定义常量和宏
#define MAX 100
#define ADD(x , y) x+y
int main(){
printf("%d", ADD(2 , 3)); //结果是5
printf("%d", 4*ADD(2 , 3)); //结果是11,这里ADD=2+3——>4*2+3=11
}
规范写法:#define ADD(x , y) ((x) + (y))
因为x和y可能是一个表达式,用括号括起来不被其它表达式、或计算影响
因为(x)+(y)是一个整体,在使用ADD()时是直接替换,不像函数,所以(x)+(y)也要被括号括起来以免被影响
5、函数
函数定义
//定义Add函数
int Add(int x, int y){
int z = 0;
z = x + y;
return z;
}
int main(){
int result = 0;
int x = 1;
int y = 2;
int Add(int x, int y); //如果这个函数在main下面,需要先声明一下,x和y可以不用写,写上类型和个数就行,一般函数都是写入在头文件中,再引用头文件
result = Add(x,y);//调用函数
}
函数参数
//数组传参
int Add( int arr[] ){ //数组名是首元素地址,传值的时候传数组名是只拿到了数组第一个地址
xxx;
//例外:
//sizeof()使用传参前的数组名时,数组名表示的是整个数组
//&arr 取地址时,也是整个数组的地址
}
6、结构体初阶
定义结构体
//结构体是对一些复杂事物的定义,相当于创建一个新的自定义类型
struct Stu{
char name[20];
int age;
} s1 , s2={ {'a'} , 11 }; // 这里可以直接创建名为s1,s2的此结构体,是全局变量,同时可以直接初始化值
int main(){
struct Stu s = {"name",20};//创建一个定义的Stu结构体
printf("%s %d", s.name, s.age);
-> //结构体指针->结构体成员
struct Stu *ps = &s;
printf("%s %d",ps->name,ps->age);
}
7、指针
内存:每个内存单元是1个字节
32位:32根物理地址线,正电=1,负电=0
64位:同32位原理
0000...0000 ——> 1111....1111一共有232个地址,**232次方个内存单元**。
int a = 10; //地址是随机分配的,一个int是4个字节,32位。
十六进制展示:0xABCD..1,打印出来的是第一个字节的地址,依次是0xABCD..2、3、4
值也是十六进制展示:0a 00 00 00
0a表示是10,现实中写为:00 00 00 0a
指针变量
//int类型的指针变量,如果控制此指针+1,那么会前进4个字节
//比如原地址是0xABCD..1,如果pa+1就会变成0xABCD..5
int *pa = &a;
//将地址解引用,找到a,并给a修改值
*pa = 20;
所有类型指针都是4个字节大小,因为32位架构的地址就是4个字节,64位则是8个字节
8、数组
数组的访问方式有 arr [n]和 n [arr]两种,[]是个运算符,所以两种方式可以,底层会自动转换
一维数组
一维数组每个元素的地址是连续存放的,随着下标增长,地址是由低到高的
int main(){
//{0},当只有一个值时,其长度内所有元素都是这个值
//当填入部分值时,其余值会默认为0
//当没填写长度时,根据填入的值确定长度
int arr[ num ] = {0}; // num必须是常量,C99语法才支持变量,俗称变长数组。
}
二维数组
二维数组,从[0][0]、[0][1]、、、[1][0]、[n][n]的地址是连续的
int main(){
int arr [num][num] = {0};
int arr [2][2] = {1,2,3,4}; //自动按顺序编排,1、2放一组,3、4放一组,没写的自动为0
int arr [num][num] = { {0} , {0} };
}
9、程序设计
以一个三子棋为例子,无具体实现,主要记录程序结构
game.h{
#include <stdio.h> //头文件内引入其它头文件,这样其它文件引入game.h时就不需要引入重复的头文件了
//常量定义
#define COL 3
#define ROW 3
//函数声明
void InitBoard( char board[ROW][COL], int row, int col );
}
test.c{
#include "game.h" //引用自定义的头文件,使用其内定义的东西
void game(){
//创建棋盘
char board[ROW][COL];
//调用初始化棋盘函数
InitBoard(board, ROW, COL);
}
}
game.c{
#include "game.h"
void InitBoard( char board[ROW][COL], int row, int col ){
//初始化棋盘操作,给每个元素设置为' ',成为空棋盘
}
}
10、指针初阶
取地址:int* pa = &a
指针大小是固定的,32位系统是4字节,64位是8字节
不同类型的指针,代表着其步进长度,如char是1个字节的大小,那么char*在+-的时候跨度为1个字节,也就是8位。
如:char* p的地址为:0x00...01,p+1则等于:0x00...02
野指针:
//1、指针未初始化,为随机值
int* p;
*p = 1; //无法执行
//2、数组访问越界也会造成野指针
arr[3] = {};
int* p = arr;
*(p+3) = x; //此时访问第四个数组下标,则会随机指向一个地址
//3、指针指向的空间释放
int* test(){
int a = 1;
return &a;
}
int main(){
int* p = test();
*p = 1; //test函数结束后,a就被释放了,p再去访问则是一个不确定的地址
}
数组指针
int arr[5] = {0};
//必须是同一块空间的指针才能进行计算
&arr[4] - &arr[0] = 4; //数组指针与数组指针相减可以得到之间的元素个数
//C语言标准规定,【数组首位地址-1】不允许和其它数组元素地址比较,【数组末尾地址+1】允许和其它元素地址比较
指针数组
int main(){
int arr[10]; //存放整形的数组->整形数组,存字符即是字符数组
int* prr[10]; //存放整形指针的数组->整形指针数组
}
指针的指针(二级指针)
int main(){
int a = 10;
int *pa = &a;
int* *ppa = &pa; // 将pa的地址存在另一个变量内,二级指针需要使用双**
*ppa == pa;
**ppa == a; //双解引用
}
11、调试技巧
Debug:包含调试信息,没有任何优化,便于调试。
——debug编译后的exe比release大,包含了调试信息。
Release:进行了优化,程序代码大小和运行速度都是最优,便于使用。(比如数组访问越界到其它变量,release会进行优化避免越界到别的变量)
windows调试技巧:
——vs快捷键:
————F5进入调试,直接跳到断点处
————F9创建与取消断点,在鼠标所选行按下,鼠标左键点行号左边也可
————F10一次执行一个过程,比如一个函数或一条语句
————F11一次执行一个语句,可以进入一个逻辑内,如进入函数内部
启动调试,工具栏调试-->窗口内可以打开断点(可以查看所有断点)、监视(查看变量的变化)、自动窗口(涉及到当前步骤的变量会自动出现在窗口内)、局部变量(同自动窗口,少于自动窗口的变量)、内存(监测内存地址、十六进制值)、反汇编(翻译成的汇编代码)
断点条件:断点右键可以设置条件,在某个条件下停止到这个断点,如在循环内设置i==n的时候停止
12、数据的存储
整型
内存存的就是补码,计算时用的补码
//正整数:原、反、补相同
//负整数:原(=>二进制取反,符号位不变)反码(=>反码+1)补码
大小端存储
0x11223344(44是低位,到11是高位)
//大端存储:低地址11223344高地址
//小端存储:低地址44332211高地址
浮点型
浮点型存储规则:
计算公式:-1^S * M * 2^E
以下解释是以32位为基础,64位的话换掉对应的值即可,如E的长度不同,对应更换中间值。
E是无符号数,所以没有负数,所以规定127是中间值,如果E是-1,那么存入E的是-1+127=126。
M在存储进去的时候,会把首位的1忽略掉,只存储后面的小数位,并且后面没有的补0
——32位
符号位:S (1bit),值是0/1
指数位:E (8bit)
有效数:M(23bit)大于等于1,小于2
——64位
符号位:S (1bit),值是0/1
指数位:E (11bit)
有效数:M(52bit)大于等于1,小于2
//代码解释存储
float f = 5.5f;
//101.1
//1.011 * 2^2
//S=0 M=1.011 E=2 这是用公式表示时各数据情况
//S=0 M=011 E=2+127 这是真实存在计算机内的值
// S:0 E:10000001 M:0110000000000000000...
//十六进制:40 b0 00 00
//小端存储:00 00 b0 40
//代码解释获取
//当E不为全0或者全1,依照存储时的方式反向取出
//当E为全1,如果M全为0,表示正负无穷大的数,取决于符号位
//当E为全0,表示接近0的很小的数字,取出时M不会加上首位去掉的1,而是就取0,E如果是-127那么存进去就是-127+127=0,取出时用1-127
例子:
int n = 9;
//000000000000...1001
float* pFloat = (float)&n;
%d,n —输出结果—> 9
%f,*pFloat —输出结果—> 0.000000
//以浮点数方式拿
//0 00000000 000...1001
//S=0 E=1-127 M=0.000000..1001
//1 * M * 2^E = 无限接近0的数,打印出来就是0.000000
*pFloat = 9.0;
//1001.0
//1.001 * 2^3 E=3
//0 10000010 00100000....0
%d,n —输出结果—> 将上面的二进制数转成了一个很大的十进制数
%f,*pFloat —输出结果—> 9.000000 浮点转浮点,值不变
13、指针进阶
数组指针+数组指针数组
指针数组(存放指针的数组)
int* arr[1]; //存放整形指针
数组指针(指向数组的指针)
int arr[1] = {1};
int (*parr) [1] = &arr; // $arr是数组的地址,arr是首元素地址
//*parr是变量为parr的指针,里面存放的是int类型数据,一共是10个int数据
数组指针解引用
int* arr[1] = {&i}; // 存放int指针的数组arr
int* (*parr) [1] = &arr; //指向上面数组的指针parr
*(*parr + 1); //*parr解引用拿到的是arr首元素地址,首元素地址+1再解引用拿到的是第二个元素值
int arr[2][3]; //二维数组
//arr代表第一个元素,第一个元素就是二维数组里面的第一个数组的地址。
//函数定义入参时,应当写成 int (*arr) [3] ,此时代表接收的是一个int类型3长度的数组地址
//入参还可以直接使用数组方式定义,int arr[2][3] ,此时传值也是首数组元素的地址
数组指针数组(存放数组指针 的 数组)
int ( * ) [2]; //这是一个数组指针,没有变量名,指向是int类型,2长度的数组
//接下来再定义一个数组,里面存放上面的数组指针
int ( * arr[3] ) [2];
//拆开来解释:
// arr [3] 是一个长度为3的数组
// 上面说过,int ( * ) [2] 是一个数组指针
// 那么arr[3] 里面 要存 int (*) [2]的数组指针
//最后得出, int (* arr[3] ) [2], arr[3]三个元素,每个元素都是int (*) [2],int (*) [2]指的是int类型长度2的数组地址。
核心思想是拆开来看,以最基础的int arr [3], arr是名字,[3]代表是数组,int代表是类型
那么上面的数组指针数组, arr是名字,arr[3]是数组,类型是int (*) [2]
数组传参
一维数组传参
//下面是前提条件和调用传参,忽略下写的顺序和格式
int arr[1] = {0};
test (arr);
void test ( int arr[] ){} //OK——形参不需要长度,可以写但没必要
void test ( int *arr ){} //OK——arr代表第一个元素地址,即 整形指针
int *arr2[1] = {0};
test2(arr2);
void test2( int *arr[] ){}//OK——arr2就是指针数组首地址,接收也是指针数组地址
void test2 ( int **arr ){}//OK——arr2是首元素地址,首元素又是一个int*,那么arr2本身就是一个二级指针
二维数组传参
int arr[3][5] = {0};
test(arr);
void test( int arr[][5] ){} //OK——这里要注意,二维长度必须要写明确,一维可以省略也可以写上
void test( int *arr ){} //不OK——因为arr是代表的int arr[5],这是第一个元素,是个整形数组
int* arr[5] //不OK——这是要接收一个指针数组
int (*arr)[5] //OK——这里接收一个int型5长度的数组指针,而arr就代表首元素,就是第一个数组的地址,第一个数组的地址那么就是数组指针
int **arr // 不OK——同上一个,arr指的是第一个数组的地址,他是数组指针,不是二级指针(二级指针里面存的是一级指针的地址)
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 字符编码:从基础到乱码解决
· 提示词工程——AI应用必不可少的技术