知识点0:Linux命令
打开终端
- 直接点击
- ctrl shift n:当前路径 注意:一定要有一个终端是打开的状态
- ctrl alt t:家目录
关闭终端
- 直接点击
- ctrl d
- exit
- 终端(打开终端之后里面有一行内容)
hq @ Ubuntu : ~/demo $
hq:用户名
@:(没什么用)
Ubuntu:主机名
: (没什么用)
~/demo:当前目录 ~:家目录
$:命令提示符
查看用户名:whoami
查看主机名:hostname
查看当前路径:pwd(绝对路径)
ls 查看当前路径下的所有文件
ls -a:查看当前路径下的所有文件包括隐藏文件 (前面是点开头的是隐藏文件)
ls -l:查看当前文件的详细信息
d rwxrwxr-x 9 hq hq 4096 3月 18 16:19 demo
文件类型 权限 连接数 用户名 组名 大小 最后一次修改的时间 文件名
文件类型
d-cbslp
b:块设备文件
c:字符设备文件 sudo cat /dev/input/mouse (/设备文件/输入文件/输入里面mouse)
s:套接字文件
l:软连接
p:管道文件
d:目录
-:普通文件
文件权限
三个三个一组
r:读
w:写
x:可执行
-:不可以
表示不可读就放一个 - 其他也是一样的,通过这个权限我们也可以得到它的权限值,这个权限值是多少:rwxrwxr-x (775)
修改权限:chmod 权限值 文件名
cd(切换目录)
cd 路径(分为绝对路径和相对路径)
绝对路径从跟目录索引
相对路径从当前路径索引
.:当前
..:上一级
cd .. :返回上一级
cd -:返回上一次路径
cd ~:返回家目录 cd cd /home/hq
新建
会有两种 新建文件 和 新建目录
新建文件:
touch 文件名.后缀
touch 同名文件:会更新时间戳
新建目录:
mkdir 目录名
mkdir 同名目录:报错 会告诉你已经存在
mkdir -p 24031/day1/linux 创建多级目录
删除
rm 普通文件名 (可以用 tab 补齐文件名)
rm -r 目录名
复制
cp 普通文件名 目标路径(可以在文件名前面加路径找到文件名 放到逆向复制的位置的路径)
cp -r 目录名 目标路径
另存为:cp 文件名 路径/新文件名
移动
mv 普通文件或者目录名 目标路径
重命名:mv 普通文件或者目录名 ./新的名字
常用的快捷键
补齐:tab (到后面我们写 mian 函数的时候直接 tab 补齐,这多方便啊)
放大终端:ctrl shift +
缩小: ctrl -
历史命令:上下键 (上一个或下一个命令)
清屏:ctrl l 或者 clear
vi编辑器
打开文件:vi 文件名
命令行模式:
编辑模式(插入模式):
底行模式:
命令行:
复制:yy 复制很多行:nyy (n:行数)
剪切:dd 剪切很多行:ndd
粘贴:p
撤销:u
反撤销:ctrl r
删除光标后面的字符:x
删除光标前面的字符:X
光标移动到首行:gg
光标移动到末行:G
光标移动到行首:0
光标移动到行尾:$
整理代码格式:gg = G
插入模式:
实现代码编辑功能
a:从光标所在位置的下一个字符键入
A:从光标所在行行尾键入
i:从光标所在位置的前一个字符键入
I:从光标所在行行首键入
o:从光标所在行下面另起一行键入
O:从光标所在行上面另起一行键入
底行模式:
指定行复制:5, 10y 复制 5 - 10 行
指定行删除:5, 10d 删除 5 - 10 行
保存:w
退出:q
保存并退出:wq
强制:! (w!、q!、wq!)
替换:s/str1/str2 光标所在行第一个str1替换成 str2
s/str1/str2/g 光标所在行所有str1替换成 str2
替换全文:%s/str1/str2/g
取消行号:set nonu
设置行号:set nu
查找:/str
取消高亮:search nohl
简单的编程步骤
- 创建一个 .c 文件 touch hello.c
- 打开文件 vi hello.c
- 写代码:
#include <stdio.h> // 头文件
int main() // 主函数 int :函数类型
{
printf("hello world!\n"); // 打印语句 \n :换行
return 0; // 函数返回值
}
- 保存并退出:wq
- 编译代码:gcc hello.c
- 执行可执行文件 ./a.out
语言历史
机器语言
汇编语言
高级语言
知识点1:gcc编译器
预处理:
展开头文件、删除注释、替换宏定义,不检查语法错误
gcc -E xxx.c -o xxx.i // xxx.c 文件名 -o 生成
编译:
检查语法错误,有错报错,没有问题会转成汇编语言,生成汇编文件
gcc -S xxx.i -o xxx.s
汇编:
将汇编文件转换成二进制文件
gcc -c xxx.s -o xxx.o
链接:
链接库文件,最终生成机器能够识别的可执行文件
gcc xxx.o -o xxx
./xxx 执行
gcc hello.c -o hello // 将 hello.c 直接生成一个可执行文件
知识点2:计算机存储单位
1字节(byte)=8位(bit)
1kb=1024b,1MB=1024kb,1GB=1024MB,1TB=1024G
知识点3:计算机数据表现形式
分类:数值型数据和非数值型数据
数值型数据:
概念:能够进行算术运算并得到确切的数值概念的一些数据
分类:
二进制<BIN> | 0101 |
八进制<OCT> | 05 |
十进制<DEC> | 5 |
十六进制<HEX> | 0x5 |
进制转换:
十进制转二进制:
- 短除法:除2倒取余
- 拆分法:拆成2的几次方和
- 计算器:程序员模式
二进制转十进制:
2的次方和,从右向左从0开始依次增加(把所有1的位加起来)
八进制转二进制:
一位8进制用3位二进制表示,以数字0开头 (可以使用421码)
二进制转八进制:
从低位开始,3个二进制为一组便是一位8进制,不够补零
十六进制转二进制:
一位16进制用4位二进制表示,以数字0x开头 (可以使用8421码)
二进制转十六进制:
从低位开始,4位二进制为一组便是一位16进制,不够补零
非数值型数据(ASCII)
在命令行输入 man ascii
\0 | 0 |
\n | 10 |
space | 32 |
0 | 48 |
9 | 57 |
a | 97 |
A | 65 |
Z | 90 |
z | 122 |
总结:'A'+32=‘a'; 'a'+25='z'; '0'-48=0;
命令行查找man手册命令:man man
退出man手册:q
知识点4:词法符号
概念:词法符号就是你在程序设置的时候在里面规定的一些由几个字符组成的一些简单的有意义的最小的语法单位。
分类:关键字,运算符,标识符,标点符号,分隔符。
关键字:
概念:由系统预定义(提前定义好的)的具有特殊功能的词法符号
关键字分类:
存储类型:auto static extern register
数据类型:char short int long float double
构造类型:struct union enum
选择结构:if else switch case default
循环结构:for while goto do 循环控制(break continue)
其他功能:
void(空类型)、typedef(重定义)、const(常量化、是只读的)、
sizeof(计算数据所占空间大小)、volatile(防止编译器优化)、
return(函数返回值)
typedef int datatype; //重定义数据类型
int a; == datatype a;
标识符
命名规则:由字母,数字,下划线组成,开头不能是数字,不能和关键字重复,尽量做到见名知义。
标点符号
, ; () 【】 {}
分隔符
tab 空格 回车
运算符:
算数运算符,逻辑运算符,位运算符,关系运算符,赋值运算符,三目运算符
算数运算符:
+ - * / % ++ --
注意(除):整数相除,向下取整(当你除号两边都是整数的时候,得到的也会是一个整数)
%:只能用于整数运算
++:
如果把它当做一个表达式来算的话,你要根据这个自加和自减的位置来判断先后运算的顺序,位置不一样先后顺序不一样(位置不同,结果不同)。--与++类似
赋值运算符:
= += -= *= /=, //a += 3; => a = a + 3;
赋值运算:
int a = 3;
int b = a++; // 3 // ++在后,先赋值,再自加
int b = ++a; // 4 // ++在前,先自加,再赋值
打印语句:
int a = 3;
- printf("%d\n", a++); // 3 // ++在后,先打印再自加
- printf("%d\n", ++a); // 4 // ++在前,先自加再打印
逻辑运算符:
也就是看你这个逻辑表达式是真的还是假的
&&(与) ||(或) !(非)
&&(逻辑与):
全真则真,一假则假
||(逻辑或):
一真则真,全假则假
!(逻辑非):
非真既假,非假即真
截断法则:
- 在逻辑与运算中:如果前面的表达式为假,则后面的表达式不执行
- 在逻辑或运算中:如果前面的表达式为真,则后面的表达式不执行
位运算符:
概念:位运算的位指的是二进制里的每一位,指的是 0 和 1,没有真假,因为他不是一个逻辑上的判断真假,而是让你去计算0和1通过这些位运算符算出来的结果到底是什么,按位进行操作
原码 | 反码 | 补码 | |
正数 | 本身 | 本身 | 本身 |
负数 | 本身 | 除符号位,按位取反 | 反码+1 |
&(位与):
全1则1,有0则0
8 & 10 = 0000 1000 = 8
8:0000 1000
10: 0000 1010
-8 & 10 = 8
-8原码:1000 1000
-8反码:1111 0111
-8补码:1111 0111 + 0000 0001 = 1111 1000
|(位或):
有1则1,全0则0
8 | 10 = 10
^(异或):
不同为1,相同为0
8 ^ 10 = 2
~(取反):按位取反 0-1
~15 = -16
15原码:0000 1111
反码: 0000 1111
补码: 0000 1111
~: 10010000(
有的读者在这一步可能有些不理解,(至少小编当时是这样的),下面我说一些对此的个人理解:
现在的原码为负数时只有变为补码的形式才可以参与二进制运算。因为15的补码是00001111,所以~15为11110000(-16的补码),但是因为~15是负数,所以现在~15是以其二进制补码形式呈现的,想得到~15的原码应该进行“回退”操作,先“减一”再进行“除符号位,按位取反”操作,最后得到10010000)
-16得到原码: 10010000
-16得到反码: 1110 1111
反码加1(补码):11110000
~-14 = 13
-14 原码:1000 1110
反码: 1111 0001
补码: 1111 0010
~: 0000 1101
<< (左移):
左移几位,右面补几个零
8 << 2 = 32 (公式:n << m = n * 2m)
>> (右移):
右移几位,左边补几个符号位(0-1)
-13 >> 1 = -7 (公式:n >> m = n / 2m)
置一公式:a | (1<<n) //第四位置一,n = 3
置零公式:a & (~(1<<n))
关系运算符:
关系运算符就是用来比较他们的大小的
> < >= <= == !=
1<x<5(这种形式是不允许出现的) => x > 1 && x < 5
三目运算符 (条件运算符)
表达式1 ? 表达式2 : 表达式3
运算法则:先判断表达式1是否成立,如果成立就执行表达式2,否则就执行表达式3
int a = 3, b = 5;
int c = a > b ? a++ : ++b; // ==> int c = ++b;
printf("%d\n", c); // 6
运算符优先级
关于运算符优先级的口诀:(由高到低)
单目运算符 ! ~ ++ --
算术运算符 * / % + -
移位运算符 << >>
关系运算符 < <= > >= == !=
位与运算符 &
异或运算符 ^ |
逻辑运算符 && ||
条件运算符 ?:
赋值运算符 = += *= /= %= ...
口诀: 单算移关与 异或逻条赋
从右向左单条赋
标点符号:
, ; () [] {}
分隔符:
换行 空格 tab
知识点5:变量
1)概念:程序运行中会发生变化的量。
2)格式: int a = 9 ;
存储类型 数据类型 变量名 变量值
((省略时)默认是auto,默认值是随机值)
存储类型:变量的存储的位置
数据类型:变量所占字节的个数
3)分类:
方面 | 全局变量 | 局部变量 |
---|---|---|
定义位置 | 整个程序 | 函数体内部 |
存储位置 | 全局区(静态区) | 栈区 |
默认值 | 未赋值时初值为0 | 未赋值时初值为随机值 |
生命周期 | 和所在的整个程序共生死 | 和所在的函数体共生死 |
作用域 | 整个程序 | 所在的函数体 |
类型 | 字节 |
char | 1 |
short | 2 |
int | 4 |
long | 4或8 |
float | 4 |
double | 8 |
知识点6:常量
概念:程序运行中不会发生改变的量
分类:
字符型常量 //''括起来
字符串常量 //""括起来
整型常量 //int
浮点型常量 //float,double
标识常量 //宏定义
指数型常量 //3e4----3*10^4
宏定义:
作用:起标识作用
命名格式:
遵循标识符的命名规则,一般用大写字母表示
格式:#define 宏名 常量或表达式
特点:只是简单字符替换,不进行运算
知识点7:输入输出
printf格式输出函数
1)函数三要素:
功能:按照指定格式向终端输出
参数:format
返回值:输出字符的个数
2)格式:
int a; //定义变量
printf("%d\n",a);
%d:按照整形格式
%hd:按短整型格式输出
%ld:按长整型格式输出;
%lld:按长长整型格式输出;
%c:按字符格式输出;
%f:按单精度浮点型格式输出;
%lf:按双精度浮点型格式输出;
%s:按字符串格式输出
%#x:按十六进制字母小写输出
%#X:按十六进制字母大写输出
%#o:按八进制格式输出)
此外%-m.n:m为位宽(若m比实际字符位数大,则按照m格式执行,多出的位宽部分用空格表示,若m比实际字符位宽小,则按照实际字符执行。
数据默认右侧对齐;-:让数据从左侧输出,.n:保留n位小数。)
\n:换行
a:变量
scanf按格式输入函数
功能:从终端按格式输入
参数:format
返回值:正确输入字符的个数(当第一个数输入格式不正确时,会直接返回0)
格式:
scanf("%d",&a);
&:取地址符
putchar按字符输出
功能:向终端输出一个字符
参数:(int a)要输出字符的ASCII值
返回值:返回输出字符的ASCII值
格式:putchar(65); // 输出A
getchar按字符输入
功能:从终端输入一个字符
参数:空
返回值:输入字符的ASCII值
格式:int a=getchar(); //向变量a输入一个字符
知识点8:垃圾字符回收
1)空格回收
功能:回收一个或多个空格,tab,或回车
格式:scanf("%d ",&a);
2)%*c回收
功能:只能回收任意一个字符
格式:scanf("%d%*c",&a);
3)getchar函数回收
功能:只能回收任意一个字符,常在循环语句中使用
格式:getchar();
扩展:
char s[32] = {};
scanf("%s", s);
*输入字符串不能含有空格*,因为scanf输入字符串遇到空格或者 \n 都会认为字符串输入结束,空格后面就不能再存放到数组里面
如果需要输入空格就按以下格式输入
scanf("%[^\n]",s); // 直到遇到 \n 才结束
知识点9:强制转换
int a=5;
float b=a/2; //b=2.000000
float b=(float)a/2; //2.500000
知识点10:分支语句
if
概念:进行条件判断,符合不同的条件去执行不同的程序
基本结构:
if(表达式)
{
// 你符合这个条件的时候,就执行语句块1
语句块1;
}
else
{
// 你不符合这个条件的时候,就执行语句块2
语句块2;
}
分层结构:
if(表达式1)
{
语句块1;
}
else if(表达式2)
{
语句块2;
}
else
{
// 都不符合条件
语句块3;
}
嵌套结构:
if(表达式1)
{
if(表达式2)
{
语句块1.1;
}
else
{
语句块1.2;
}
}
else
{
语句块2;
}
总结:
1)if后面可以没有else,但是else前面必须有if
2)if和else后面的大括号都可以省略,但是省略大括号后只能匹配后面一条语句
switch
基本结构
switch(变量或表达式)
{
case 常量1: 语句块1; break;
case 常量2: 语句块2; break;
case 常量3: 语句块3; break;
...
case 常量n: 语句块n; break;
// 如果以上常量都不符合的情况下
// 就执行 default 后面的语句块
default: 语句块 n+1;
}
注意:
switch()里的表达式不能是浮点型或字符串
2)case后面的break可以省略,但是省略后程序会顺序执行直到遇见break结束或运行结束
知识点11循环语句
for循环
基本结构
// for循环后面不要使用 分号
for(表达式1; 表达式2; 表达式3)
{
语句块;
}表达式1:赋初值
表达式2:循环的终止条件
表达式3:增值减值语句
嵌套结构
for(表达式1; 表达式2; 表达式3)
{
for(表达式4; 表达式5; 表达式6)
{
语句块7;
}
}
基本结构的变形
变形一
int i = 0;
for(;表达式2; 表达式3)
{
语句块;
}
变形二
int i = 0;
for(; 表达式2; )
{
语句块;
表达式3
}
变形三
int i = 0;
for(; ;) // 死循环
{
if(表达式2)
{
语句块;
表达式3;
}
else
{
break;
}
}
for(int i=0;i<10;i++);
//计算个数值(实际可能需要在循环后(外)执行i=i-1)
while循环
定义循环变量并赋值;
while(判断条件)
{
语句块;
增值或减值语句;
} //先判断再执行
do-while循环
定义循环变量并赋值
do
{
语句块;
自增自减语句;
}while(终止条件)
//先执行后判断
死循环
for(;;){};
while(1){};
while(1); // 让程序卡死在这里等待
循环控制语句
break:直接结束循环
continue:结束本次循环,进入下一次循环
使用场景:使用在循环语句中,结束循环
注意:使用时需要有判断条件
知识点12:数组
概念
具有一定顺序的若干变量的集合
定义格式
存储类型 数据类型 数组名【元素个数】
int arr[5];
数组名:代表数组首元素的首地址,arr是地址常量不能被赋值
访问元素
数组名【下标】,下标从0开始
访问第一个元素:arr[0];
访问第n个元素:arr[n-1];
特点
数组的元素数据类型相同
内存连续
注意
1)数组的数据类型就是数组元素的数据类型
2)数组名要符合标识符的命名规则
3)在同一个函数中,数组名不要与变量名相同
4)下标从0开始,到n-1结束
分类:
一维数组和二维数组
一维数组
概念:只有一个下标的数组
格式:
存储类型 数据类型 数组名【元素个数】
int arr[5];
访问元素(上同)
数组名:数组的首地址
初始化
1)全部初始化
int arr[5]={1 2 3 4 5};
2)部分初始化
int arr[5]={1 2}; //1 2 0 0 0
//初始化的部分正常赋值,未初始化部分值为0
3)未初始化
int arr[5]; //这样就是随机值
arr[0]=1;
arr[1]=2; //进行单个赋值
变量当数组下标情况:(不推荐)
如:
int a=10;
int arr[a]; // 用变量定义数组长度可以,但是不可以初始化
int arr[a]={}; // 会报错
定义空数组
1)全部初始化
int arr[5]={0 0 0 0 0};
2)部分初始化
int arr[5]={0};
3)空数组
int arr[5]={};
引用
1)先定义后引用
2)每次只能引用一个数组元素arr[i],如果想引用所有元素可以循环遍历
int arr[5]={1,2,3,4,5};
for(int i=0;i<5;i++)
{
printf("%d\n",arr[i]);
}
3)引用时防止数组越界,虽然有时编译器不会报错
printf("%p\n",&arr[i]); //打印数组元素的地址用%格式
内存分配
数组遍历
把循环变量作为数组下标,用for循环遍历
int arr[5] = {};
int i; // 循环变量
for(i = 0; i < 5; i++)
{
scanf("%d", &arr[i]);
}
for(i = 0; i < 5; i++)
{
printf("%d", arr[i]);
}
printf("\n");printf("%d\n", arr[5]); // 数组越界,错误
数组大小
1)数组元素个数*数据类型大小
2)sizeof(数组名)
int arr[5];
printf("%d\n",sizeof(arr)); //20字节
计算元素的个数:
数组大小:
int arr[5]; // 20
double b[2]; // 16
char c[32]; // 32
1.数组元素个数 * 数据类型大小
2.sizeof(数组名)
printf("%d\n", sizeof(arr)); // 20 字节
计算元素的个数:
1)sizeof(数组名) / sizeof(数据类型)
清零函数(字符串)
1)bzero
#include <strings.h>
void bzero(void *s, size_t n);
功能:将空间设置为0
参数:s:要清空的空间的首地址
n:字节大小
返回值:无
2.memset
#include <string.h>
void *memset(void *s, int c, size_t n);
功能:将内存空间设置为0
参数:s:要清空的空间的首地址
c:要设置的值,设置为0
n:字节大小
返回值:要清空的空间的首地址
知识点13:字符数组
(字符数组可以存放字符串)
1.概念:
元素的数据类型为字符型的数组
2.形式:
char a[] = {'a', 'b', 'c'}; // 逐个字符的赋值,sizeof(a) == 3
char b[] = {"hello"}; // 用字符串赋值,sizeof(b) ==6// 因为字符串后面会默认补一个\0,所以是6
char c[] = "hello"; // 用字符串赋值,sizeof(b) == 6char str[32] = "hello"; // szieof(str) == 32
char d[]; // 这样是不可以的,会报一个没有数组大小
char d[] = {}; // 大小是0,随便放一个字符就会越界
注意:字符串赋值常常省略数组的长度,需要注意数组越界问题
3.字符数组输入输出
输入
char s[32] = {};
1)scanf("%s", s);
*输入字符串不能含有空格*,因为scanf输入字符串遇到空格或者 \n 都会认为字符串输入结束,空格后面就不能再存放到数组里面
如果需要输入空格就按以下格式输入
scanf("%[^\n]",s); // 直到遇到 \n 才结束
2)循环遍历
for(int i = 0; i < 5; i++)
{
scanf("%c", &s[i]);
}
3)gets
#include <stdio.h>
char *gets(char *s);
功能:从终端获取字符串
参数:s:目标字符数组的首地址
返回值:目标字符数组的首地址
gets(s); // gets在输入的时不关心数组越界问题,使用时会报警告
char s[32] = {};
gets(s);
printf("%s\n", s);
输出
1)printf("%s\n", s);
2)循环遍历
for(int i = 0; i < 32; i++)
{
printf("%c", s[i]);
}
3)puts
#include <stdio.h>
int puts(const char *s);
功能:向终端输出字符串
参数:s:要输出字符数组的首地址
返回值:输出字符的个数
// 如果不需要返回值直接调用函数就可以了
puts(s);
char s[32] = {};
gets(s);
puts(s);
判断:
char s[10] = {};
s[10] = "hello"; // 错误,数组越界
s = "hello"; // 错误,s是地址常量,不能为左值strcpy(s, "string"); // 正确
int n;
for(n = 0; buf[n] != '\0', n++); // 计算元素的个数
计算字符串的实际长度
1.for循环遍历数组,直到 \0 为止
for (n = 0; buf[n] != '\0'; n++); //元素个数
2.strlen
#include <string.h>
size_t strlen(const char *s);
功能:计算字符串的实际长度(不包含 \0)
参数:s:要计算字符串的首地址
返回值:字符串的实际长度
char buf[32] = {"hello"};
int num = strlen(buf);
printf("%d\n", num); // 5
sizeof(buf); // 32
sizeof和strlen的区别?
本质上 | sizeof是关键字 | strlen是函数 |
功能上 | sizeof是计算数据所占空间大小 | strlen是计算字符串的实际长度 |
使用时 | sizeof计算包括\0 | strlen不包括\0,所以计算字符串长度时(元素个数省略的情况下)sizeof比strlen大1 |
知识点14排序
冒泡排序
两两比较,第i个和i+1个比较
int a[5] = {5, 4, 3, 2, 1};
第一轮: i = 0 n个数:比较n-1轮,每一轮交换的次数是 n - 1次开始依次递减的
4 5 3 2 1
4 3 5 2 1
4 3 2 5 1
4 3 2 1 5
第二轮: i = 1
3 4 2 1 5(内层循环 j<5-1-1
3 2 4 1 5
3 2 1 4 5
第三轮: i = 2 (内层循环 j<5-1-2
2 3 1 4 5
2 1 3 4 5
第四轮: i = 3
1 2 3 4 5
总结:外层循环 i<n-1
内层循环 j<n-1-i
选择排序
n 个数:先找出最小值的下标暂存,选择出最小的值与arr[i] 个数交换
排序过程:
1. 首先通过n-1次比较,从n个数中找出最小值的下标,将它与第一个数交换,第一轮选择排序,结果最小值被放在第一个元素的位置上
2. 再通过 n-2 次比较,从剩余的 n-1 个数中找出最小值的下标做记录,将它与第二个数交换,第二轮的排序
3. 重复上述过程,共经过 n-1 轮排序后,排序结束
知识点15二维数组
1. 格式
存储类型 数据类型 数组名[行数][列数]
int arr[2][3];
2. 访问元素:
数组名[行下标][列下标];(下标从0开始)
arr[0][0]:第一行第一列的元素
arr[1][2]:第二行第三列的元素
行下标和列下标都不能越界
行数可以省略,列数不能省略
int a[][3] = {1, 2, 3, 4, 5, 6};
3. 数组的元素个数
行数*列数
二维数组的大小 / 数据类型的大小
4. 二维数组的大小
sizeof(数组名)
数据类型的大小*行数*列数
5. 数组名
arr:第一行首地址
arr+1:第二行首地址
arr+n:第n+1行首地址
6. 初始化:
1) 全部初始化:
int arr[2][3] = {1, 2, 3, 4, 5, 6}; // 顺序赋值
printf("%d %d %d\n", arr[0][0], arr[0][1], arr[0][2]); // 1 2 3
printf("%d %d %d\n", arr[1][0], arr[1][1], arr[1][2]); // 4 5 6
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
}; // 按行赋值
2) 部分初始化:未被初始化的元素值为0
int arr[2][3] = {1, 2, 3, 4}; // 顺序赋值 1 2 3 4 0 0
int arr[2][3] = {{1, 2},{4, 5}}; // 按行赋值 1 2 0 4 5 0
3) 未初始化:随机值,需要单独赋值
int arr[2][3];
7.内存分配
如图为一个5行6列的二维数组
// a 和 a[0] 并不完全相同,级别不同,后面会讲,数值上是一样的,级别不一样
// a 是行地址
// a[0] 是列地址
// a 表示第一行的首地址
// a+1 表示第二行的首地址
// a[0] 表示第一行第一列的地址
// a[1] 表示第二行第一列的地址
8. 二维数组遍历
for循环嵌套,外层行数,内层列数
int a[m][n] = {};
for(int i = 0; i < m; i++) // 行下标
{
for(int j = 0; j < n; j++) // 列下标
{
scanf(); 或者 printf();
}
}
// 写一个 for循环嵌套 a[2][3] 从终端输入,并输出,也可以直接初始化,从终端输出
指针
指针的优点:
\1. 使程序更简洁、紧凑、高效
\2. 有效的表达更复杂的数据结构
\3. 动态分配内存
\4. 得到多于一个数的函数返回值
1. 概念
地址:内存中每个字节单位都有一个编号(门牌号)
指针:指针就是地址
指针变量:用于存放地址的这种变量就叫指针变量
2. 格式:
存储类型 数据类型 * 指针变量名
int *p; // 定义了一个指针变量p
例子:
int num = 5;
int *p = #
char ch = 'c';
char *q = &ch;
3. 指针操作符:
&:取地址符,取变量的地址
*:取内容,取地址里面的内容
*&a == a; // *和&都是单目运算符,互逆运算
// a是变量就是错的,a是地址就是对的
&*a; // 错误:(运算符的优先级)
4. 初始化:
指针变量在使用前不仅要定义还要初始化,未初始化的指针变量不能随便使用,会产生野指针
4.1. 将普通变量的地址赋值给指针变量
int a = 10;
1) int *p = &a; // 在定义的同时赋值
2) int *p = NULL;
p = &a; // 先定义后赋值
4.2. 将数组的首地址赋值给指针变量
char str[10]="hello";
char* p=str;//指针指向了数组的首地址,即指向了字符'h'
char str[10] = "hello";
char *p = str;
printf("%p %p\n", p, str);
printf("%c %c\n", *p, *str);
4.3. 将指针变量里面保存的地址赋值给另一个指针变量
float a =1.3;
float* p=&a;
float* q=p;
5. 指针运算
5.1. 算术运算
char str[32]="hello";
char* p=str;
p++(赋值);
//指针向高地址方向移动一个数据单位,指针指向发生变化
p--; //指针向低地址方向移动一个数据单位,指针指向发生变化
例子:
int* p;
p++;
//移动4个字节
double* p;
p++;
//移动8个字节
char str[10] = "hello";
char *p = str;
printf("%p %p\n", &str[0], &str[1]);
p++;
printf("%p\n", p);
printf("%c %c\n", *p, *str);
(运算)
p+n: 向高地址方向访问第n个数据的地址,指针指向不发生变化
p-n:向低地址方向访问第n个数据的地址,指针指向不发生变化
偏移了多少地址(字节)=n*sizeof(数据类型)
偏移了多少地址(字节) = n * sizeof(数据类型)
两个地址之间的差 == 两个地址之间相隔的元素个数
q - p = 之间相隔的元素个数
int m = 100;
double n = 200;
int *p1 = &m;
int *p2 = p1 + 2;
double *q1 = &n;
double *q2 = q1 + 2;
printf("p1=%p p2%p\n", p1, p2);
printf("p2 - p1 = %d\n", p2-p1);
printf("q1=%p q2%p\n", q1, q2);
printf("q2 - q1 = %d\n", q2-q1);
5.2. 关系运算
< == !=
指针之间的关系运算比较的是它指向地址的高低
指向高地址的指针大于指向低地址的指针
char str[32] = "hello";
char *p1 = &str[1];
char *p2 = &str[3];
p2 > p1;
注意:指向不同类型数组的指针进行关系运算时没有意义,
指向不同区域的指针的关系运算也没有意义
(同一个数组之间进行比较)
6.指针的大小
int a = 5;
int *p1 = &a; // 4
short b = 2;
short *p2 = &b; // 4
double c = 1.11;
double *p3 = &c; // 4
double *p4 = NULL; // 4
printf("%d\n", sizeof(p4));
32位操作系统:指针大小为4字节
8位16进制表示,4字节
64位操作系统:指针大小为8字节
16位16进制表示,8字节
总结:
1.32位操作系统:指针大小为4字节,64位操作系统,指针大小为8字节
2.内存地址是固定,但是但是变量的地址不固定(栈区随机分配变量)
3.指针类型根据指针指向空间的数据类型
Segentation fault (core dumped
产生原因:访问不存在的内存地址、访问系统保护的地址、访问只读的内存地址、
空指针废弃(eg:malloc 与 free释放后,继续使用)、栈堆溢出
内存越界(数组越界、变量类型不一致等)
7.指针修饰
1. const 常量化
1) const int a =10;
int const a = 10;
a = 20; // 错,a的值不能改变,可以通过指针间接修改
// 因为 const 没有修饰 *p,修饰的是a
int *p = &a;
*p = 20; // 可以
2) const int *p; // 修饰 *p,指针指向的内容不能修改,指针指向可以修改
int const *p;
int a = 10;
const int *p = &a;
2.1) *p = 20; // 错误,因为 *p 被修饰
2.2) int b = 20;
p = &b; // 正确
3) int *const p; // 修饰 p,指针指向不能被改变,指针指向的内容可以修改
int a = 10;
int b = 20;
int *const p = &a;
3.1) *p = 20; // 正确
3.2) p = &b; // 错误,因为修饰的是 p,指针指向不能改变
4) 修饰函数的参数
2.void
void a; // 不允许修饰变量
void *p; // p 任意类型的指针
使用场景:函数参数或者函数返回值
注意:通过void类型的指针进行取内容的时候,需要对地址进行强转
转换方式:void *p = NULL; 强转:(int *)p 取内容:*(int *)p
int a = 10;
void *p = NULL;
// (int *)p = &a; // 错误 对指针进行类型强转需要在赋值右面
p = &a;
int *q = (int *)p;
printf("%d %d\n", *q, *(int *)p); // 10 10
8.大小端
概念:在计算机进行超过1字节数据进行存储时,会出现存储数据顺序不同的情况即大小端存储
Big-Endian(大端字节序)大端:在低地址存放高字节数据,高地址存放低字节数据
Little-Endian(小端字节序) 小端:在低地址存放低字节数据,在高地址存放高字节数据
举例:存储数据:0x12345678, 起始地址:0x4000
0x4000 0x4001 0x4002 0x4003
常见的大小端判断方法有:数据类型强转、指针强转、共用体
二级指针
概念:
一级指针:存放变量的地址
二级指针:存放一级指针的地址(指针变量的地址)
格式:存储类型 数据类型 **指针变量名
如
int a = 10;
int *p = &a;
int **q = &p;
指针和数组
直接访问:按变量的地址存取变量的值(通过数组名访问)
间接访问:通过存放变量的地址的变量去访问元素(通过指针访问)
-
指针和一维数组
int a[5] = {1, 2, 3, 4, 5};
int *p = a;
直接访问:
int a[5] = {5, 4, 3, 2, 1};
int *p = a;
printf("%p %p %p\n", a, a+1, a+2);
printf("%p %p %p\n", p, p+1, p+2);
printf("%d %d %d\n", a[0], *(a+1), *(a+2));
间接访问:
int a[5] = {5, 4, 3, 2, 1};
int *p = a;
printf("%p %p %p\n", a, a+1, a+2);
printf("%p %p %p\n", p, p+1, p+2);
printf("%d %d\n", *(a+2), *(a+3));
printf("%d %d\n", *(p+2), *(p+3));
printf("%d %d\n", a[0], p[0]);
注意:a和p本质上不同,a是地址常量,p是变量,a不能执行++操作,但是p可以
访问数组元素a[i]的值:
直接访问:a[i] *(a+i)
间接访问:p[i] *(p+i)
访问数组元素a[i]的地址:
直接访问:&a[i] a+i
间接访问:&p[i] p+i
例如;
int a[3] = {3, 2, 1};
int *p = a;
printf("%d\n", *p++); // 3 再打印一次的话就是2
printf("%d\n", *a++); // 错误,a地址常量
运算方法:
- ++和 * 都是单目运算符,优先级相同
- 运算顺序从右向左进行运算
int a[3] = {3, 2, 1};
int *p = a;
printf("%d\n", 下列打印);
*(p++) // 3 实际上指针指向第二个元素的地址
(*p)++ // 打印出来的是3 实际上第一个元素值变为4
++*p // 打印出来的是4 自加完之后的值
++(*p) // 同上
*++p // 2,先将会指针向高地址方向移动一个数据单位,然后取地址内容
*(++p) // 同上
2.指针和二维数组
int a[2][3] = {1, 2, 3, 4, 5, 6}; // a数组名,表示第一行首地址,a+1:第二行首地址
在a前面加 *,表示将行地址降级为列地址
*a:第一行第一列的地址
*a+1:第一行第二列的地址
*(a+1):第二行第一列的地址
*(a+1)+1:第二行第二列的地址
直接访问:
*(*(a+i)+j):拿到i+1行j+1列的元素
*(a[i]+j):拿到i+1行j+1列的元素
a[i][j]:拿到i+1行j+1列的元素
间接访问
数组指针
定义:本质上是指针,指向的数组(行指针)
格式:存储类型 数据类型 (*指针变量名)[列数];
int a[2][3] = {1, 2, 3, 4, 5, 6};
int (*p)[3] = a;
p:int (*)[3]:运算是三个三个算的
p可以代替a进行元素访问,但是本质不同
访问 a[i][j] 地址:
p[i] + j == a[i]+j
*(p+i)+j
访问 a[i][j] 的内容
*(*(p+i)+j) *(p[i] + j)
大小:
sizeof(p) == 4字节(32系统)