目录
一、Ubuntu的使用
Linux系统文件分类:
- 普通文件(图片, 视频, 音频, world文档, txt文档, c文件....)
d 目录文件(文件夹)
c 字符设备文件(驱动课程学习)
b 块设备文件(驱动课程学习)
l 链接文件(类似快捷方式 - linux学习)
p 管道文件(进程时学习)
s 套接字文件(网络编程学习)
注意:inux不以后缀区分文件 - 后缀是给程序员自己区分的
终 端
终端介绍:
hqyj @ ubuntu : ~ $
用户名 分隔符 主机名 分隔符 路径缩写 命令提示符(普通用户的提示符)
root用户(管理员用户)的命令提示符: #
Linux系统终端命令:
命令使用:命令本身 [参数选项] [路径/位置/文件夹名]
pwd 自我定位, 查看当前位置/路径
ls 查看当前文件夹下的 文件
ls -l 查看文件的详细信息
ls -a 查看所有文件, 包含隐藏文件
ls / -a -l 查看根目录下有哪些文件, 包括隐藏文件, 以及文件的详细信息
sudo 命令 普通用户暂时使用 管理员权限 去执行命令
su 切换到管理员用户
su 用户名 切换回普通用户 su hqyj
clear 清屏命令 快捷方式: ctrl + l
exit 关闭当前终端 快捷方式: ctrl + shift + q
cd 目录文件名 进入到目录文件中, 前提是这个目录文件存在, 且在当前路径下
cd 路径+目录文件名 cd /dev 切换到根目录下的dev目录下
cd .. 回到上一级路径
绝对路径: /home/hqyj/class/ 从根目录出发, pwd查看到的路径位置
相对路径: 从当前路径出发, 进入目标路径
cd - 回到上一次操作路径
cd 后面什么都不加, 直接回到用户目录(家目录 /home/hqyj/) 下, 一键回家
mkdir 目录文件名 创建空白的目录文件(文件夹)
rmdir 目录文件名 只能删除空白的目录文件, 删除后找不回来
rm -r 目录文件名 可以删除空白目录文件, 也可以删除非空目录文件, 彻底删除
rm 文件名 删除普通文件, 彻底删除
touch 普通文件名 创建空白的 普通文件 , 一般自己加上后缀名
如果文件存在, 会更新文件的时间戳, 不会改变文件内容
cat 文件名 将文件内容打印到终端屏幕上
man 命令/函数 查看帮助手册/说明文档
快捷方式
ctrl + alt + t 打开一个全新的终端, 默认为主用户目录
ctrl + shift + n 打开一个新的终端, 和上一个终端路径相同, 必须在已有一个终端的情况下使用
ctr + shift + t 打开一个标签, 并列打开一个终端, 路径和上一个路径相同
alt + tab 切换不同的应用
alt + n(数字) 切换到第 n 个标签
F11 / Fn + F11 将终端全屏 运行, 再次按下退出全屏
ctrl + '+' / ctrl + shift + '+' 放大终端字体
ctrl + '-' 缩小终端字体
tab 自动补全 命令 或 文件名
键盘上的 上下箭头 翻看之前执行过的命令
ctrl + 'c' 强制结束程序
编写程序
编写/写代码(使用编辑工具) - 编译(将程序员认识的语言翻译成计算机语言/二进制) - 运行
编辑工具: gedit 类似记事本(文本文档编辑工具)
vi / vim 程序员专用编写代码工具
编译工具: gcc
gcc c文件名
编译成功会默认生成 a.out 可执行文件(二进制文件)
运 行 ./a.out (运行可执行文件)
vi / vim 使用
分为三种模式: 命令模式 编辑模式 底行模式
命令模式: 刚进入 vi 编辑器处于 命令模式, 看不见的命令
yy 复制光标所在一行的内容
p 将内容(复制/剪切)粘贴到光标所在的下一行
dd 剪切/删除光标所在一行的内容 键盘上的方向键 移动光标位置
u 撤销
G 跳转到文档末尾位置
gg 跳转到文档起始位置
gg=G 文档代码格式对齐
进入编辑模式的命令:
i 插入, 在光标前一个字符进行插入
a 在光标后一个字符进行插入
o 在光标的下一行进行插入
I 在光标所在行最前面进行插入
A 在光标所在行的最后面进行插入
O 在光标的上方进行插入
s 删除光标所在字符后 进行插入
S 删除光标所在行后进行插入
退出编辑模式, 回到命令行模式
Esc 一个按键(键盘左上角)
进入底行模式(命令)
输入冒号( shift + : ), 冒号后跟命令, 看的见的命令
:4,10y 复制4到10行的内容
:4,10d 剪切/删除4到10行的内容
:n,my n m是数字, 复制n到m行的内容
:n,md n m是数字, 剪切/删除n到m行的内容
:w 保存编写内容
:q 退出vi操作
:wq 保存并退出
:q! 强制退出(未保存)
:wq! 强制保存并退出
:x 保存并退出
:n n是数字, 光标跳转到第n行
set mouse=v 让鼠标能够选中复制
set mouse=a 让鼠标能够控制光标位置
二、C语言基础
程序的编译
gcc demo1.c
改变可执行文件名
gcc -o 可执行文件名(自己命名) c代码
gcc c代码 -o 可执行文件名(自己命名)
eg: gcc -o 1 demo1.c
gcc 编译的详细步骤:
预处理 --> 编译 --> 汇编 --> 链接
预处理
进行头文件的展开, 宏替换, 去除注释等预先处理步骤, 不会进行错误检查, 会生成C语言原始程序(demo.i)
gcc -E c代码 -o 原始文件.i
gcc c代码 -E -o demo.i
编译
查找语法错误, 检查无误后, 将 c语言原始程序 准换为 汇编文件(demo.s)
gcc 原始文件.i -S -o 汇编文件.s
汇编
将 汇编文件 转换为 目标文件(机器能识别的语言, 二进制文件)
gcc 汇编文件.s -c -o 目标文件.o
链接
链接程序运行时一些必要的库文件, 生成可执行文件
gcc 目标文件.o -o 可执行文件名
非数值数据 - 字符
特殊字符
0 -- '\0'
10 -- '\n'
32 -- ' ' 空格
48 -- '0'
65 -- 'A'
97 -- 'a'
词法符号
关键字(32个)
char : 声明字符型变量或函数
short : 声明短整型变量或函数
int : 声明整型变量或函数
long : 声明长整型变量或函数
float : 声明浮点型变量或函数
double : 声明双精度变量或函数
signed : 声明有符号类型变量或函数
unsigned :声明无符号类型变量或函数
enum : 声明枚举类型
struct : 声明结构体变量或函数
union : 声明联合数据类型
void : 声明函数无返回值或无参数,声明无类型指针
if : 条件语句肯定分支
else : 条件语句否定分支(与 if 连用)
switch : 用于分支语句
case : 开关语句分支
default : 开关语句中的“其他”分支
for : 一种循环语句(可意会不可言传)
while : 循环语句的循环条件
do : 循环语句的循环体
goto : 无条件跳转语句
break : 跳出当前循环
continue : 结束当前循环,开始下一轮循环
return : 子程序返回语句(可以带参数,也看不带参数)
auto : 声明自动变量,在栈区开辟空间, 一般不使用
static : 声明静态变量
extern : 声明变量是在其他文件正声明(也可以看做是引用变量)
register : 声明寄存器变量,申请失败,默认转成auto类型
const : 修饰变量为只读变量
volatile : 修饰变量,读取最新的值(防止编译器优化;从内存读取最新的值)
sizeof : 计算数据类型占内存大小
typedef : 用以给数据类型取别名(当然还有其他作用)
运算符
算数运算
+ - *(乘) /(除) %(取余)
关系运算
判断两个数或两者之间的关系
关系成立则结果为真(1), 关系不成立则为假(0)
> < >= <= !=(不等于) ==(等于)
逻辑运算
逻辑 - 将数据看作一个整体, 只有 真(1 / 非零数据) 和 假(0)
&&(逻辑与) : 前后都为真, 则结果为真, 有假(0)则假(0)
||(逻辑或) : 有真(1)则真(1), 有一个为真则结果为真, 前后都为假, 则结果为假
!(逻辑非) 真变假, 假变真
短路法则:
(1>2)&&(3+4) 因为(1>2)为假, 结果一定为假(0), 不会去计算(3+4)
(1<2)||(3+4) 因为(1<2)为真, 结果一定为真(1), 不会去计算(3+4)
位运算
位 - 指的是 二进制 的位, 符号位要参与运算, 浮点数不能进行位运算
&(位与) |(位或) ~(位取反) ^(异或) >>(右移运算) <<(左移运算)
赋值运算
将 等号 右边的值 赋值给 左边 的变量
=
复合赋值运算
+= -= *= /= %= >>= <<= &= |= ^=
自增自减运算符 ++ --
条件运算 三目运算符
(条件判断)?(结果为真执行):(结果为假执行)
求字节大小
计算 变量/数据 占内存空间的大小 (以字节为单位)
sizeof( 变量/数据 )
逗号运算 : 从左到右依次执行, 取最右边的结果
运算符优先级
分隔符
起到分隔作用, 将不同的词法符号分开
通过对分隔符的恰当运用, 使得代码外观格式更为清晰易读, 还可以帮助分析代码中的语法错误
标点符号
; 分号作为一条语句结束标志
: 控制语句中有特殊作用(case goto)
{} 作为模块的划分, 还用于数组初始化中
() 控制语句中有特殊作用
, 作为运算符存在
标识符
用户(程序员)自己定义的有特殊含义的符号
用于定义 变量名 函数名 宏名 自定义类型名等...
命名规则:
由 字母 数字 下划线 组成
不能以数字开头(只能以字母和下划线开头)
不能和关键字重名
常量:
程序中不可被改变的数据量
1 整形常量
2 浮点型常量
3 字符常量 单引号框起来
4 字符串常量 双引号框起来, 字符串以'\0'作为结束标志, '\0'是存在的, 至少默认省略(看不见)
5 标识符常量(宏) 利用标识符 来 表示 常量
变量
程序中可以被改变的数据量
计算机在内存中开辟一片空间给程序员存放数据, 空间内的数据可以被改变
定义:程序员在代码中向系统申请空间
[存储类型] [数据类型] [变量名] ;
存储类型: 规定了变量存放的区域
数据类型: 规定的变量存放空间的大小
变量名: 对存储数据的空间取的名字, 要符合标识符命名规则
存储类型:
auto 自动存储类型
int n; //默认就是自动存储类型, 通常省略
auto int m;
去定义全局变量时, 将变量存放在全局变量区, 不初始化全局变量, 值默认为0
去定义局部变量时, 将变量存放在栈区 , 不初始化局部变量, 值是随机值
register 寄存器存储类型
将变量放到寄存器中, 不在内存中
寄存器数量少, 运行速度快, 可能申请失败
static 静态存储类型
修饰全局变量时, 会限制变量只能在本文件内使用, 限制变量的作用域
修饰局部变量时, 会将变量存放到全局变量区, 延长变量的生命周期
extern 外部引用
只能引用外部的全局变量, 两个文件用的是同一个变量
const : 将变量常量化
修饰局部变量, 变量的值只读, 不能被修改
修饰指针, 根据const的位置不同, 功能也不同
修饰函数的参数, 说明参数在函数内部不能被修改
数据类型:
基本数据类型: 单个数据
整形: char short int long
浮点型: float double
字符型: char
枚举型: enum, 会自动赋值, 默认从0开始, 后面依次是前一个加一的结果
构造数据类型: 集合
数组: 一次性定义多个相同类型变量, 在空间内连续存放
结构体: 存放不同类型变量的集合, 每一个变量有自己独立的空间
共用体: 存放不同类型变量的集合, 所有变量共用一片空间
特殊数据类型:
指针: 存放地址的 变量
void: 用于修饰指针, 表示不确定类型的地址; 用于修饰函数, 表示无返回值或无参数
数据在内存中的存储
数据是以 补码 的形式存放在内存中, 主要看 数据的值/数据本身
取出数据使用时看 数据类型 或 输出格式
正整数和0: 对于 无符号数 都是正整数
源码 == 补码
负整数:
源码就是 直接转为 二进制
反码: 源码取反, 不改变符号位
补码: 反码加一
类型转换
注意: 所有的转换都是当时有效, 并不会改变 变量的 数据类型 和 值
显式转换: 看得见的类型转换
强制类型转换: (数据类型)
注意:
强制类型转换是一种不安全的转换, 将高精度转为低精度时, 会产生数据丢失
强制类型转换后的数据如果是复杂运算, 一定要用圆括号框起来
隐式转换: 看不见的类型转换
不同数据类型在进行运算时, 会进行隐式转换, 将不同类型转为同一类型
将低精度转换为高精度, 有符号转换为无符号
三、 输入输出函数
输出: 将程序内容打印到屏幕上
输入: 将键盘输入的字符 传输 到程序中
printf() - 标准格式输出函数
使用时要添加头文件 #include
格式1: printf("要输出的字符"); //直接原样输出, 注意转义字符
格式2: printf(" 字符串 格式控制串 ", 输出表 );
字符串会原样输出
格式控制串会由变量的值替换
输出表就是要输出的数据(变量名或地址)
格式控制串: 按照一定的格式去输出变量的值, 通常由 格式符 和 修饰符 组成
% 作为格式控制串开始符号, 想要输出一个%, 使用"%%"
格式符: 主要控制输出格式, 必不可少
%d 将变量的值以 有符号十进制整数 输出
%u 输出无符号十进制整数
%o 输出无符号八进制整数
%x 输出无符号十六进制整数, 字母用小写表示
%X 输出无符号十六进制整数, 字母用大写表示
%c 输出单个字符
%s 输出字符串, 字符串一般保存在数组内, 后面的变量就应该是数组名
%f 输出小数, 默认输出小数点后6位, 会四舍五入
%e 输出科学计数法, 默认输出小数点后6位, 会四舍五入
%g 输出 小数 和 科学计数法 中短的一种
%p 输出变量的地址
修饰符: 起到修饰作用, 可用可不用, 要用到恰当的地方
# 用于%o %x, 自动添加前缀
+ 用于%d, 将正整数前加上符号(+)
m m代表数字, 控制域宽, 对齐格式
用于所有格式, 输出字符m 原样输出, 默认右对齐
- 用于所有格式, 和m搭配使用, 输出字符
0 用于所有格式, 和m搭配使用, 输出字符
l 用于%d,%f, 用于输出 长整型(long) 和 双精度浮点数(double)
h 用于%d, 用于输出 短整型(short)
.n 用于%f %g %e, 控制小数点后位数
puts() - 输出字符串
输出一行内容
输出字符串时会自动换行
输出提示信息时不方便
putchar() - 输出单个字符
putchar(70); //输出 ASCII 对应的字符
getchar() - 输入单个字符
只能获取到一个字符, "回车键"作为结束标志
注意: 括号内不能写内容!!!
通常用于 回收垃圾字符(多次连续输入产生的'\n').
gets() - 输入字符串
获取一行字符, 保存到变量内, 遇到"回车键"结束, 能输入空格
但是是一个危险的函数, 字符数组(保存多个字符/字符串)都有容量限制, 但是gets()函数无法检查这个限 制
编译时会报警告, 运行时可能会造成段错误, 或数据错误
scanf() - 格式控制输入
scanf("格式控制串", 输入表);
格式控制串: 格式符 + 修饰符
输入表: 一定是地址
注意:
对于整数和浮点数来说: 回车键是最终的结束标志, 空格也可以作为两次输入的分隔
对于整数和浮点数, 遇到不认识的字符(非法字符), 会提前结束
整数: , @ # $ %....qw, 浮点数: %^&*,kkll
对于%c字符来说, 回车键是最终的结束标志, 但是连续多次输入时, 会读取到上一次的'\n'(垃圾 字符)
对于%s, 空格就结束了, 字符串不能输入空格
双引号内的字符串需要原样输入, 所以最好不要有
scanf("n:%d", &n); "n:" 需要自己手动输入
最好写成printf("n: "); scanf("%d", &n);
四、控制语句
顺序语句: 代码都是从上往下顺序执行的, 一行可以放多条语句, 但是为了代码的易读性, 通常一行就放 一条语句, 一条语句以分号作为结束标志
分支语句: 选择语句 if-else switch-case-default
循环语句: for while do-while
辅助控制语句: continue break goto retrun
if-else
基本格式要求:
注意:
if 下只有一条语句时, {} 可以省略, 但是建议初学者自己写代码时都加上花括号
else 只能和if搭配使用, 后面没有圆括号
if( 条件判断 )
{
//条件判断的结果为 真, 则执行花括号内的语句;
} else //只能和if搭配使用, 不能独立存在
{
//if括号内"条件判断"的结果为 假, 则执行else后花括号内的语句;
}
switch-case
基本格式要求:
注意: case 后面只能跟 字符常量 或 整形常量 或 字符串常量, 不能是浮点数,不能是表达式
'a' 字符常量 12 整型常量 0x3 整型常量 "hello" 字符串常量
恰当运用 break, 结束语句执行
如果语句中没有break, 会执行匹配结果向下所有的执行语句
每个 case 后 常量 的值必须各不相同,否则会出错
switch( 变量/表达式 )
{
//常量表达式 一定是 字符常量 或 整形常量 或 字符串常量, 不能是浮点数,不能是表达式
// 'a' 字符常量 12 整型常量 0x3 整型常量 "hello" 字符串常量
case 常量: 执行语句; break;
case 常量: 执行语句; break;
case 常量: 执行语句; break;
case 常量: 执行语句; break;
case 常量: 执行语句; break;
default: 执行语句;
}
while
基本格式要求:
while( 判断语句 )
{
//循环语句
//判断语句 结果 为真, 则一直执行
}
do-while
基本格式要求:
注意: 最后要加 分号
先执行一次, 再进行判断 ( 密码 输入)
do{
//循环语句
//判断条件为真, 则进入循环
}while( 判断条件 );
for
基本格式要求:
注意: ()括号内 三条语句 用 分号 间隔开
初始语句1 只执行一次
()括号外没有分号
for( 初始语句1; 判断条件2; 每次循环后都会执行的语句3 )
{
//循环体;4
//判断条件为真, 则进入循环5
}
//语句6
1. 先执行 初始语句1
2. 执行 判断条件2, 结果为真, 执行循环体 4 5, 执行语句 3, 执行 判断条件2
3. 执行 判断条件2, 结果为假, 不进入循环, 执行循环后的语句6
break
用在 switch 语句中, 用于提前结束语句
用于循环内, 提前结束循环
continue
只能作用在循环语句中, 用于结束本次循环, 提前进入下一次循环
goto
无条件跳转语句
谨慎使用, 注意不要陷入死循环
会造成代码可读性差, 通常用于错误判断
return
用于函数内部, 结束函数, 在函数章节具体讲解
五、数组
数组: 具有 一定关系 的 多个 相同类型变量 的 集合, 并且在内存空间上连续存储
一维数组
定义:
存储类型 数据类型 数组名[数组能存放的最大元素个数]
数组名: 代表了数组这个整体, 数组名也就是数组的首地址常量
注意:
方括号内 一定是 常量, 数组的大小一定是固定的, 使用时一定不要超过界限
使用数组时超出 数组范围, 运行程序时可能会报段错误(已放弃 (核心已转储) / 系统崩溃)
编译时 不会检查 段错误
段错误又称为 非法访问 内存
字符数组存放字符串时注意给'\0'留位置
使用
数组名[下标];
注意:
下标都是从0开始的, 最大只能到 "数组能存放的最大元素个数" - 1
初始化
完全初始化: 给数组中的每一个元素勾 赋上初值
部分初始化: 按顺序给前面的元素赋值, 后面没有给是给初值的元素就默认为零
缺省初始化: 省略 方括号的 元素个数, 数组的最大元素个数 就是 花括号内的元素个数
计算
数组总的占空间的大小 / 单个元素占空间的大小
一维数组的遍历
将数组中 的 每一个元素的值 全部打印到屏幕上
int i = 0;
int num[10] = {0};
for(i=0; i<10; i++)
{
printf("%d ", num[i]);
}
printf("\n");
二维数组:
多个 相同大小 的 一维数组 的集合
定义:
存储类型 数据类型 二维数组名[一维数组的个数][一维数组中元素的个数];
存储类型 数据类型 二维数组名[行][列];
初始化
完全初始化: 给数组中的每一个元素勾 赋上初值
部分初始化: 按顺序给前面的元素赋值, 后面没有给是给初值的元素就默认为零
缺省初始化: 只能缺省 行 / 第一个数字
使用
数组名[下标][下标]
数组名[行][列]
数组名[下标] 等价为一维数组名
计算
int num[10][45] = {0};
行数:
sizeof(num) / sizeof(num[0]); //10*45*4 / 45*4 ==> 10
列数: //一般不用求, 因为列数不能缺省
sizeof(num[0]) / sizeof(num[0][0]);
所有元素个数: 行*列
sizeof(num) / sizeof(num[0][0]); //10*45*4 / 4 ==> 45*10
遍历数组:
输出数组中的所有元素:
int n = 行数, m = 列数;
int i = 0, j = 0;
for(i=0; i<n; i++)
{
for(j=0; j<m; j++)
{
printf("%d ", num[i][j]);
}
练习1: 杨辉三角
printf("\n");
}
int i = 0, j = 0;
for(i=0; i<n; i++)
{
for(j=0; j<m; j++)
printf("%d ", num[i][j]);
printf("\n");
}
字符串函数:
strcpy() - 拷贝
strcpy(str1, str2); //将str2中的字符串拷贝到str1中
strlen() - 求字符串长度
不计算'\0'的存在, 计算的是字符串的有效长度
strcat() - 拼接
将 后一个字符串 连接到前一个字符串后面
strcmp() - 比较函数
比较字符串 中 每一个字符的ascii值
六、指针
地址: 计算机对内存空间的编号(1字节) 相当于 门牌号
指针: 本质就是地址, 相当于 对 地址的抽象称呼
指针变量: 特殊数据类型, 专门 保存地址 的变量
一级指针
定义:
存储类型 数据类型 * 指针名;
eg:
int * p; //只能保存 int 类型 变量的地址
char * q; //只能保存 char 类型 变量的地址
double * f; //只能保存 double 类型 变量的地址
short * m; //只能保存 short 类型 变量的地址
数据类型: 指的是 指针 保存地址 中的数据类型
数据类型 体现的是 读取数据的方式, 读取数据的单元个数(空间大小), 读取几个字节的内容
int * 是一种新的数据类型, 指针类型, 去修饰p变量, 说明"p变量"存放的是一个"int类型变量"的地 址
使用:
& 取地址, 取变量的地址, 要确保后面跟的是变量
* 解引用, 取地址中的内容, 要确保 * 后面跟的是 地址
* 和 & 是相反的作用, 使用正确的情况下, 可以互相抵消,(注意位置)
int num = 0;
*&num (正确) &*num (错误)
p 是指针变量 , 也是地址
*p 是一个int类型的普通变量, 一个普通元素
指针的运算:
1 算数运算:
+ -
int *p = NULL;
int *q = NULL;
p+n // 向高地址偏移 n*sizeof(数据类型) 个字节 (int -- n*4) (double -- n*8)
p-n // 向低地址偏移 n*sizeof(数据类型) 个字节
p+n // 向高地址偏移 n 个数据
p-n // 向低地址偏移 n 个数据
//p q 必须是相同类型, 才能做减法
p-q // 结果为long int类型, 是两个地址之间相差的数据个数. (p-q)/(sizeof(数据类型))
(p + 12); //p变量(指针)的值没有发生变化, 只是临时偏移
(p += 12); //p = p+12 p变量(指针)的值发生了改变, 会保存 12个 元素后的地址
(p -= 12); //p = p-12 p变量(指针)的值发生了改变, 会保存 12个 元素前的地址
p++; //p = p+1;
p--; //p = p-1;
++p; //p = p+1;
--p; //p = p-1;
2 关系运算
> < >= <= != ==
p > q //判断的是地址的高低
int * p == NULL;
if(p == NULL)
{
//不能访问
}
void 修饰的指针:
void *p = NULL; //保存 任意类型地址的 指针
p = &n; //n可以是任意数据类型
//但是后续使用时, 必须进行强制类型转换
(* ((数据类型 *)p) ) ++;
const 修饰的指针
将变量 常量化, 使得变量不能被修改
const修饰指针变量时, 根据位置不同, 不能修改的内容也不同
const int * p = NULL; //限制 *p 的内容不能被修改
int const * p = NULL; //限制 *p 的内容不能被修改
//注意要 在初始化时, 给p 赋值一个 可以操作的地址空间, 不能指向NULL
int * const p = &n; //限制 p 的内容不能被修改, 也就是不能改变 p 的指向(内部的地址)
二级指针
存放 一级指针地址 的指针变量
基本格式: 存储类型 数据类型 * * 指针名;
多级指针
保存 指针变量地址的 变量
指针和一维数组
指针: 地址变量
一维数组名: 地址常量, 数组首元素的地址