程序基本结构
所有的的程序都从它开始
#include <stdio.h>
int main(){
return 0;
}
- 一个C语言程序,都是由函数和变量构成的
- 每一个程序都是从 main 函数开始执行的,这意味着每个程序都要包含 main 函数
- main 函数的返回值必须为 int
注释
// 单行注释
/* 多行注释 */
变量
在C语言中,变量必须声明才能使用
声明用于说明变量的属性
// 声明
int count;
// 定义/初始化
int count = 1;
整体结构
类型 变量名 = 初始值
这里有人喜欢称变量名为标识,都是同一个意思。
变量类型
C是静态的,没法动态的识别变量属性。都需要我们手动的设置变量的类型。
常用类型
- int 的整数
- float 浮点数/小数
- char 字符
变量基础类型与长度参考表里面有更加详细的介绍
类型限定符
用于限制变量的长度或者是特点
- unsigned 这个类型的值总是限定为 正数或者0(就是自然数)
- signed 表示有符号的,就是有正负 ± 符号
- short 比 int 长度小
- long 比 int 长度大
这两个可以在 int 前面,但我们通常不写 int 直接写
short a; long b;
隐式转换
隐式转换又称自动转换,在表达式中是将占位小的转成表达式中占位最大的那个类型
尽管如此,float 类型是不会自动转换成 double 类型的,因为双精准度的运算特别费时
从"宽"到“窄”
如果你要从占位大的转换成占位小的,那么溢出的部分将会被截取。
例如:float 转 int,小数部分就会被截取
函数传参
建议先看完函数再回来看
在函数传参时,也有可能发生类型转换
例如:void test(int a, double b)
如果传入 char 或 short 就会转成 int, float 会转成 double
字符转数字
建议看完字符常量在回来看
如果你的字符表示的是一个数字,那么你可以用这样的方法
‘- 0’ 是为了转成数字,那为什么不直接 '4' - 0
呢?
因为 ‘4’ 的机器码为 52 要减去机器码为48的 ‘0’,才能得到 4
'4' - '0' - 0
字符 = 机器码中相对应的整数
所以我们可以改变它的数值来获取机器码相对于的那个字符
但我们可以通过隐式转换来改变
unsigned char a = '1';
int b = 123;
a = b;
// a = '{'
为了保证程序的可移植性,如果要在 char 类型的变量中储存非字符数据,最好指定 signed 或 unsigned限定符
强制转换
表达式为
(类型名) 表达式
(double) n
虽然是强制转换,但不会影响变量本身,除非你重新赋值。
float i = 1.111;
i = (unsigned int)i;
1.111 变成 1.000
这也就可以将转换的值赋值给变量了,但是变量类型为 float 的事实不会被改变。
静态变量
建议先看完函数再回来看
是用 static 关键字声明的变量/函数,声明之后,变量/函数只在当前作用域(范围)内生效,无法从外部访问。
外部通常是指外部文件和函数外的地方。因为这样的特性,通常用来隐藏变量。
局部静态变量
static 类型的内部变量是一种只能在某个特定函数中使用但一直占据储存空间的变量
可以在局部变量(通常是函数内)定义变量
例如:
int max(){
static int a = 1;
a++;
return a;
}
// main 中调用
printf("%d", max()); // 2
printf("%d", max()); // 3
printf("%d", max()); // 4
printf("%d", max()); // 5
在局部使用 static 声明的变量和自动变量不同
局部静态变量是不会随着函数执行完消失的。
寄存器变量
寄存器变量是用 register 关键字声明的变量
它的意思是告诉编译器,这个变量在程序中使用频率较高。其思想是,将 register 变量存放在机器的寄存器中,这样可以使程序更小、执行速度更快。
寄存器有着,每个函数最多有多少寄存器变量和只允许某些变量设置寄存器变量的限制。
可以过多的声明,因为不同的机器有不同的限制,它们(编译器)可以忽略过量或者不支持的寄存器变量声明
需要注意的是:无论寄存器变量是否储存在寄存器中,它的地址都无法访问!
全局变量
建议先看完函数再回来看
可以使用 extern 声明使用全局变量
如果变量定义在函数前,那么可以不用使用 extern
值得注意的是:除非有多个函数共用外部变量,否则尽量使用局部变量(自动变量)
int max(int a, int b);
int a = 1;
int main(){
// b 在函数下方创建所以需要 extern
extern int b;
max(a,b);
return 0;
}
int b = 2;
int max(int a, int b){
return a > b ? a : b;
}
在一个好的程序设计风格中,应该避免出现变量名隐藏外部作用域中相同名字的情况,否则,很有可能引起混乱和错误
输入输出
printf 是最常用的输出结果方法
C语言本身并没有定义输入/输出功能,printf 仅仅是标准库函数中的一个有用的函数
这里函数和方法说的都是同一个意思
格式化输出
百分号 + 字母
是基础的格式,每个字母代表输出不同的类型
printf("%d", count);
printf("%d%s", count, name);
百分号 + 有字符数字 + 字母
有符号的意思是可以有正负,正是左对齐;负是右对齐 例如:-1 就是右对齐占1个格子
数字代表 %d 的宽度(占位度)
printf("%5d %-3d", num, pop);
百分号 + 小数 + 字母
.2 -> 表示小数点后面有2位数字(或者说保留两位数字)
printf("%5.2f %.2f \n", num, pop);
格式化输入
可以利用 scanf 函数来实现
且看例子:
int i, j;
scanf("%d%d", &i, &j);
这里 & 是取地址符,可先不理,带看完指针,回头看就明了了。
最常用的格式符
- d 输入 整数
- s 输入 字符串
- c 输入 字符
- f 输入 浮点数
使用时需要注意:
- 参数的个数、类型、顺序一定要对应
- 除了格式符之外不要写其他东西
字符的输入输出
字符的输入输出是可以考函数实现的
- getchar() 获取字符
- putchar() 输出字符
当 getchar 没有输入时会返回 EOF (end of file)
它的作用很明显,用来判断是否为空。
putchar 甚至还可以输入整数来打印字符,毕竟字符就是字符常量,也就是一个整数。
if ((c = getchar()) != EOF){
putchar(c)
}
// putchar(123);
// {
运算
在整数运算中,整数除法结果不保留小数。
算术运算符
包括 + - * / %
其中 %(取模)常用来做限定范围或者取整判断
限制 x 只能在 100 的范围内
x % 100
判断是否为3的倍数
if (count % 3 == 0){ 语句 }
关系运算符和逻辑运算符
关系运算符
包括 > < >= <=
逻辑运算符
- && and
- || or
- ! 非
位移运算符
位移运算符为 << (左移动) >> (右移动)
如果是带符号(signed)类型的值的规则:
- 右移: 左边空缺的部分用符号位填补(算数移位)
- 左移:右边空缺的部分用0填补(逻辑位移)
unsigned 类型的值,不管左右移动,都用0去填补
左右移的规则:
- 左移:
1 << 2
等同于 1 ∗ 2 2 1 * 2^2 1∗22 结果为 4
也就是说n << x
等同于 n ∗ 2 x n * 2^x n∗2x - 右移:
8 >> 3
等同于 8 / 2 3 8 / 2^3 8/23 结果为 1
也就是说n >> x
等同于 n / 2 x n / 2^x n/2x
赋值运算符
赋值运算符是赋值表达式中使用二元运算的简写
例如:
int a = 1;
a += 2;
// 相当于 a = a + 2
int b = 2;
b *= a + 3;
// 相当于 b = b * (a + 3)
运算符的优先级
如果代码的执行结果与求值顺序相关,则都是不好的程序设计风格。
因为不同的机器的求值顺序不同,最好不要用特殊的方式实现,以免出错了却找不出问题。
我们更加偏向使用 () 去调整优先级,这样更明了。
a[i] = i++
这样的代码很容易让人混淆、头大,这里我们应该先提取 i++ 用临时变量储存的。
C语言运算符优先级有更详细的介绍
三元运算符
在需要返回值却无需做多行处理的时候非常有用
a == 1 ? true : false
// 等价
if (a == 1){
return true;
} else {
return false;
}
前后缀运算符
在数字运算时可以代替,a+=1
类似的式子
规则:讲究先后顺序而已
- ++nc 先自增再使用 nc 的值
- nc++ 先使用 nc 的值再自增
int nc = 0;
int a = ++nc;
int b = nc++;
// a = 1
// b = 1
// nc = 2
程序控制流程语句
在表达式之后加上一个分号,它们就变成了语句。其中分号是语句的结束符。
花括号(“{” 和 “}”)内的多行语句构成了符合语句(又叫程序块)
循环语句
字面意思,就是用来做遍历、枚举、循环的语句
for语句
for语句是一种循环语句
for(表达式1; 表达式2; 表达式3){
语句
}
// 例子
for(int i=0; i < 10; i = i + 1){
;
}
圆括号中有三个部分,各个部分之间用分号隔开。
int i = 0
这个部分是初始化部分
i < 10
第二部分 是控制循环的测试或条件部分
i = i + 1
第三部分是增长步长部分
- 虽然这三部分都可以是任何表达式,但是我还是不建议在这三部分中写和循环不相关的代码。
- 在写循环语句时,应当利用好条件判断部分,以减少循环次数占用资源
- 在代码块中的分号是单独的分号,称之为空语句
虽然这三个部分都可以省略,但是其中的 ;; (分号)是必不可少的
且如果省略了 表达式2(判断条件部分)那么就认为永远为真,如果不 break 就死循环
for (;;){}
while 语句
while 语句也是循环语句,虽然语法简洁,但是在实现上面 for 比 while 好很多。
// 基本语法
while(表达式){
语句
}
for(表达式1; 表达式2; 表达式3){
语句
}
// 转换成 while 的语句实现,常用结构
表达式1
while(表达式2){
语句
表达式3
}
do while 语句
这个语句会先执行循环体(do 的代码块)中的复合语句,然后再计算 while 括号里的表达式。如果表达式成立,那么将再次执行以此类推
也就是说,无论如何都会先执行一遍。
在对于判断用户输入时很有用,先让用户输入再做判断。
语法:
do{
语句
} while (表达式)
break 和 continue 语句
在循环语句中,有两种语句 break(终止循环)和 continue(继续循环)
区别:
- break 直接跳出当前循环
- continue 跳过后面的代码,进入下一次循环
值得注意的是,这两个语句只在当前循环层生效
goto 语句
使用 goto 语句会让代码变得难以理解和维护,使用我们应该尽量避免使用
goto 语句一般常用于跳出多循环体
例如:
for (...){
for (...){
if (...){
goto cope;
}
}
}
...
cope:
// 代码跳到这里继续往下执行
...
这种情况下我们应当使用 “开关” (就是使用 bool 变量去判断 去判断,更加合理且符合逻辑
for (...){
for (...){
if (...){
is_cope = true;
}
}
}
...
if (cope == true){
...
}
...
判断语句
用于条件判断,处理 真 与 否 做出相应的操作。
if 语句
if 语句作用于条件判断
在执行中,会先计算表达式内的值,只会有一些两种结果。
- 为真(非0)就执行语句后面的 else 都不执行
- 为假就到下一个 else
代码中的 else if 是分支(多路判断),末尾 else 则是“当上面条件都不成立的情形处理”;
else if 和 else 如果不需要都可以忽略不写;else 常用来处理错误
if (表达式)
语句
else if(表达式){
语句
}
else{
语句
}
值得注意的是:
尽管当语句只有一行时,我们可以省略花括号,但严谨的程序设计中,建议都使用花括号。
switch 语句
switch 语句是用来判断常量的多路判断,它只可以判断数值和字符且只判断是否相等
switch (表达式){
case 常量表达式: 复合语句
case 常量表达式: 复合语句
default: 复合语句
}
// 例子
var a=3,b=4;
switch(a)
{
case 1:b++;break;
case 2:++b;break;
case 3:b++;
case 4:++b;
case 5:b++;break;
default:b++;
}
// b = 7
其中 case 为分支(多路判断),末尾 default 和 if 的 else 相似,它的意思是“没有哪一分支匹配常量表达式的默认值”
值得注意:
在例子中,我们可以注意到 b 的值与我们想的不太一样。这是 break 造成的,如果 case 或者 default 没有在复合语句中加入 break 来终止语句的执行,那么它将会一直往下执行。
为了避免这种情况,我们应该在复合语句的结尾都加上 break; 来调出语句
当然在函数中,我们也可以使用 return 来调出
缺少调出语句是不健全的,而且这样会很大的造成错误。我们应该尽量减少这种“不健全”写法。
常量
常量是不可以被表达式改变的值,但是可以引用(访问)常量。
常量一般都是大写。当有多个设定值供全局使用时,可以定义多个常量,使代码更加的语义化。
常量表达式:仅仅是包含常量的表达式
#define
#define 指令可以把符号名(或者称为符号常量)定义为指定的值
#define MAX 10
#define NAME "123sfasd"
大部分的值为数值或者是字符串类型,当然也是可以代码块(函数)
值得注意:
- #define 命令后面没有分号;
- #define 是预处理指令并不会检查替换内容的错误,所以维护性差
const 关键字创建
const int A = 1;
字符常量
单个字符表示一个整数,该值是字符在机器字符集中对应的数值,称之为字符常量
字符常量一般用于与其他字符比较,但也可以用于数值运算
在 ASCII 码中就有 A 对应 65 数值
char a = "A";
a = a + 32;
// a = "a"
转义字符序列也是合法的字符常量
例如 ‘\n’ 就是单个字符常量
字符串常量
字符串常量也叫字符串字面量,就是字符数组。
字符数组往往以 ‘\0’ 结尾,实际上就是为空
char a[10]="hello word"
值得注意的是: ’x‘ 是一个字符(字符常量), “x” 是一个字符串(字符数组)
字符常量对比
在判断是否是数字时,这样的对比是很有必要的
if (a >= '0' && a <= '9'){
语句
}
枚举常量
枚举常量用于解决多个 #define 定义常量
枚举是一个常量整型值的列表,就是说如果你不给值,那就是递增的整数值。
但也意味着,你不能定义除了整型之外的值
enum bool = { No, Yes}
从0开始递增
指定值
enum months = { Jan = 1, Feb = 2, Mar = 3}
也可以从指定的递增值
这里可以直接写成
enum months = { Jan = 1, Feb, Mar}
函数
函数的作用是封装复杂的逻辑处理,使用时无须考虑如何实现的快捷方式。
快速了解
函数的创建与使用, 分别为定义原型、声明函数、使用函数
注意:函数中不允许定义函数
// 声明
int max(int a, int b);
int min(int, int);
int main(){
// 使用
max(1,2);
min(1,2);
// 2
// 1
return 0;
}
// 定义原型
int max(int a, int b){
return a > b ? a : b;
}
int min(int a, int b){
return a > b ? b : a;
}
函数名前的 int 是值函数返回的类型,通过 return 语句返回值。
函数不一定要有返回值,在原型和声明中使用 void 即可。
return (表达式)
返回值会被转化成函数的返回值类型,表达式常会别加上圆括号,但它可以被省略
函数需要定义才能使用,这种定义称之为函数原型
int min(int a, int b){
return a > b ? b : a;
}
圆括号内的变量,我们称之为形参(形式参数),函数调中对应形参的值为实参(实际参数)。
需要注意:
- 在使用函数之前需要声明和定义,否则无法使用;
- 声明和定义里的函数名和参数的类型和数量要一致;
- 在声明里,形参名字可以和原型不一致;例如
int min(int, int);
- 在函数中创建的变量我们称之为自动变量,因为它在每一次调用完时消失。
如果函数声明中不包含形参,则会关闭所有的形参检查。为了避免这种现象,我们需要:
如果函数带有参数,则要声明他们;如果没有参数,则使用 void 进行声明
递归
递归的意思,即是函数可以直接或间接调用自身
递归的执行顺序是以**栈(后进先出的列表)**的形式,也就是说,
有调用自身就先放入栈中,等没有调用自身的代码了就可以继续运行并出栈
我们用下面一个例子说明,我们设计了一个函数,它用来打印数字
void printNum(int num){
if (num < 0){
// 小于零就先打印符号,并把它变成正数
putchar('-');
num = -num;
}
// 判断到最高位(最左边)函数进栈
if (num / 10){
printInt(num / 10);
}
// 如果是最高位就开始打印 函数出栈
putchar(num % 10 + '0');
}
// main 中调用
int a = -456123;
printNum(a);
// -456123
预处理
预处理是在编译程序之前做的处理
最常用的两个指令是 #include 和 #define
文件包含
文件包含的具体指令
#include "文件名"
// 或
#include <文件名>
这个指令将会被替换成 文件名 指定的文件的内容。如果 文件名 用引号引起来,则在源文件所在的位置查找该文件;如果 文件名 用尖括号括起来,则将根据相应的规则查找改文件。
宏替换
宏定义为:
#define 名字 替换文本
这个只是最简单的宏替换,只是将程序中所有出现 名字 的地方替换成 替换文本
简而言之是直接 插入 代码中。
替换的文本没有限制,具体看下列的例子:
#define forever while(1) /* 无限循环 */
#define MAX(x, y) ((x) > (y) ? (x) : (y))
这里添加圆括号是为了保证程序运行的正确性,如果你不先计算 x 和 y 中的表达式再去比较,那么你很有可能会出现不可预料的问题。
如果你先将值替换成带引号的字符串,那么你可以在引用时加上 #
例如:
#define dpint(msg) printf(#msg "\n")
// main 中调用
dpint(abcbcbcbbcbcb);
// 输出 abcbcbcbbcbcb
注意:dpint 括号内填的不是变量名,可以随意传入值,即使是表达式都无所谓。
数组
数组即是储存变量的列表
快速了解
分别定义一维和二维数组:
// 当有初始化的时候可以忽略长度
int mouth[]= {1,2,3,4,5};
// 二维数组是 [行][列] 每行有多少列是需要声明的
int mouths[][3]= {{1,2,3},{4,5,6}};
// 可以省略花括号
int mouths[][3]= {1,2,3,4,5,6};
// 如果只是声明,就要明确指出所以数值
int mouth[10];
int mouths[2][3];
// 通过下标获取数值中的元素,下标的起始值是 0
mouth[0] // 第一个元素
mouths[1][0] // 第二行第一个元素
字符数组
在将字符串转化成字符数组时,会在结尾添加 ‘\0’ (即是空字符,值为0),用来标记结尾。
字符数组还可以简化写法:
char msg[]="iyou";
// 相当于
char msg[]={'i', 'y', 'o', 'u'};
指针
指针是一种保存变量地址的变量
如果不明白可以先看看底层动画演示
单一变量与指针
指针操作最常用的 * 一元运算符号是间接寻址或间接引用运算符。我们常称为取值符
在单一变量指针中还有一个常用的符号是 & 取地址符,顾名思义就是获取变量的地址
定义并使用指针改变原来的值
int a = 1;
// 定义指针变量,注意类型
int *p;
// 通过取地址符获取地址
p = &a; // *p = 1
// 修改 a 的值
*p = 3; // a = 3
printf("%d", *p); // *p = 3
// 指针也可以赋值成其他的指针
int a = 1, b = 2, *p, *q;
p = &a;
q = &b;
p = q;
// *p = 2
补充 &: 地址运算符 & 只能应用于内存中的对象,即变量和数组元素,它不能作用于表达式、常量或 register 类型的变量
指针与一维数组
用指针编写的程序比用数组下标编写的程序执行度快,但另方面,用指针实现的程序难以理解
声明一个数组
int a[10]
定义了一个长度为 10 的数组 a。换句话说,它定义了一个由 10 个对象组成的集合,这 10 个对象存储在相邻的内存区域中,名字分别为 a[0]、a[1]…
数组指针的定义另有不同
int a[] = {10,12};
int *p;
p = &a[0];
// *p = 10
// 获取上一个元素
p = &a[1];
printf("%d", *(p-1));
// 10
上面代码的指针指向的是 a 数组的第一个元素。
根据指针运算的定义,p + 1 将指向下一个元素,同理 p - 1 将指向上一个元素
根据这条原理,可以使用取值符获取下一个元素的值:*(p+1)
指针与二维数组
在此之前,我们需要先知道一个概念
数组名就是该数组的第一个元素地址。且数组地址是不变的,可以理解为常量。
声明一个2行4列的二维数组
int a[2][4];
定义了一个长度为 2 * 4 的数组 a。名字分别为 a[0][0]、a[0][1]…
int a[2][4] = {{13,21,35,47}, {98,43,56,67}};
int *p;
p = &a[0][0];
printf("%d", *p);
printf("%d", *(p+4));
// 98
// 获取上一个
p = &a[1][0];
printf("%d", *(p-1));
// 47
相信你们也注意到了,使用指针去获取数组中的元素时,方式是跟一维数组是一样的。
- 先定义指针的位置
- 通过指针运算 + 1 或 - 1 去分别获取下一个和上一个元素
示意图:
二维数组指针从行开始
看完简写之后再来看
上面我们是将指针 - 1, + 1 分别获取上一个和下一个元素。但是我们想通过 - 1,+ 1 来分别获取上一行和下一行。
我们可以这样写:
int a[2][3] = {{21,4324,3543}, {764, 943, 123}};
// 注意看定义方式
int (*p)[3];
p = a;
for(int i =0;i < 2; i++){
for (int j=0; j < 3; j++){
// i 是行 j 是列
printf("%d\t", *(*(p+ i)+ j));
}
printf("\n");
}
在上述中,我们使用 int (*p)[3]
去定义实际上相等于 int p[][3];
其中 p[]
= *(p+0)
= *p
注意,(*p)[0]
中的括号是必不可少的,因为 [] 的优先级高于 * 的优先级。
数组指针的简写
我们前面有提到这么一个概念
数组名就是该数组的第一个元素地址。
那么我们可以推出这么两个简写方式
// 赋值给 指针
&a[i] = a + i
// 通过 语法糖 做表达式
a[i] = *(a+i)
根据这个依据我们可以将数组的定义稍加改变
// 一维数组
int a[] = {10,12};
int *p;
// 旧 p = &a[0]
p = a;
// 旧 p = &a[1]
p = a + 1;
// *p = 12
int a[2][4] = {{13,21,35,47}, {98,43,56,67}};
int *p;
// 旧 p = &a[0][0]
p = a[0] + 0 = *(a+0) = *a;
// 旧 p = &a[1][2];
p = a[1] + 2 = *(a+1) + 2 = *a + 1 * 4 + 2 = *a + 5;
如果需要遍历的话,使用指针是非常方便的。这里贴一张猴博士课上的总结图
修改传输参数
在函数拿到的参数,只是函数私有的(作用域内)临时副本的值。
如果希望在函数中的修改能影响到原始参数值,可以使用指针来改变
void changeA(int *b);
int main(){
int a = 1;
// 注意这里是 &a 传入地址
changeA(&a);
// a = 20
return 0;
}
void changeA(int *b){
*b = 20;
}
如果修改二维数组可以这样写:
void fun(m[3][15])
= void fun(m[][15])
= void fun((*m)[15])
void printList(char (*msgs)[15]){
// 修改
**msgs = 'p';
*(*(msgs + 1) + 13) = 'm';
// 打印
for(int i = 0; i < 3; i++){
printf("%s \n", *(msgs + i));
}
}
// main
void printList(char (*msgs)[15]);
char msgs[][15] = {
"focus on table",
"introduction u",
"approve to you"
};
printList(msgs);
// pocus on table
// introduction m
// approve to you
提醒一下:我声明的列数是有 14 但是区区字符却只有 14 个,那么第十五个正是 ‘\0’ 字符串的结尾。
地址运算符
指针也是变量,他们之间的赋值、运算、比较都是有意义的。
数组指针最佳实践
在之前,我们使用过数组的指针进行加减可以获得 上下元素。
但是我们还可以通过 高 - 低
的方式来计算他们之间相隔的元素。
我们使用下面案例来说明:
// 定义了一个整数类型的数组,来存放整数
static const int MAX_LENGHT = 3;
static int a[MAX_LENGHT];
static int *p = a;
int main(){
// 声明 add 函数
void add(int num);
add(15387);
add(123);
add(213);
add(1432);
// 通过指针的运用,输出数组全部的元素
int *q = a;
while (*q != '\0' && *q != 0){
printf("%d\n", *q);
q++;
}
return 0;
}
// 往数组里添加整数
void add(int num){
// 声明函数
int getLenght();
if(getLenght()){
// 我觉得这里有必要做一下解释
// 因为 * 和 ++ 是从右到左结合的,但 ++ 是后自增的,所以这里是先
// *p = num; 再 p++
*p++ = num;
} else
{
printf("不够地方了\n");
}
}
// 获取数组的长度
int getLenght(){
// 通过 数组名(初始地址) + 最大长度 - 指针变量(添加元素时有增加赋值,此时代码的就是数组中最后一个元素的位置)
// 此处就是 高 - 低 的最佳实践
return a + MAX_LENGHT - p > 0 ? 1 : 0;
}
输出值:
不够地方了
15387
123
213
这个案例其实就是对一个数组进行添加元素,一旦没有地方放元素了,就做提示,并不再加入元素。
需要注意:如果指针 p 不属于 a 数组,那么这些计算将毫无意义。
变量的指针的上下获取
我们可以想象,这样的定义变量,就像加入了一个列表,只不过这个列表加入时,是从前加入的。
就像是 js 中向列表开头插入的函数 unshift
char a = 'a';
char b = '3';
char c = '6';
char q = '0';
// ['0', '6', '3', 'a']
char *p = &b;
printf("%c\n", *(p-1));
// '6'
指针算数具有一致性
也就是说,如果你中间有不同元素,那么你这样的操作将毫无意义
char a = 'a';
char b = '3';
char c = '6';
int q = 12312;
int pw = '0';
char *p = &b;
printf("%c\n", *(p-3));
// 空
字符数组的指针
相对于字符,我们更多的是使用字符串,而字符串其实就是字符数组。
char *msg;
msg = "ily";
char ass[] = "ily";
这里的 msg 其实就是等于 [‘i’, ‘l’, ‘y’]。需要注意的是,字符数组都已 ‘\0’ 作为结尾。
在这个赋值中,msg 获取的其实是数组第一个元素的下标作为指针变量。
因此如果通过 msg 去修改字符串中的内容是没有意义的。
指针数组
试想一下,如果我们将指针放入一个集合里,要更改只需交换他们的指针即可。
那么这将比单纯的交换两个值带来的消耗更小。
例子:
char a[] = "shot by huawei";
char b[] = "employee you advantage";
char c[] = "no cases of there";
// 指针数组的初始化
char *msgs[] = {
a,
b,
c
};
// 交换 b 和 c
char *tmp;
tmp = *(msgs + 1);
*(msgs + 1) = *(msgs + 2);
*(msgs + 2) = tmp;
// 改变其中 c 的第2个字母为 p
*(*(msgs + 1) + 1) = 'p';
// 打印列表
for(int i = 0; i < 3; i++){
printf("%s \n", *(msgs + i));
}
// shot by huawei
// np cases of there
// employee you advantage
你们可能也注意到了,我虽然没有声明列数,但是我的每个元素的长度是可以不一样的。
如果你要正常的声明,可以是这样的。
char msgs[][15] = {
"focus on table",
"introduction u",
"approve to you"
};
很明显,他们都需要长度一致。
所以使用指针数组的好处之一,就是每个元素的长度是可以不一致
我这里并没有使用上面那种快速定义的方式,而是选择了使用变量引用的方式。是因为我们不仅要读取还要做修改。如果你只是需要读取,那么你大可以用快速定义的方式。
指向函数的指针
在C语言中,函数本身不是变量,但可以定义指向函数的指针。这种类型的指针可以被赋值、存放在数组中、传递给函数以及作为函数的返回值等等
例子:
double add( double x, double y ) { return x + y; }
// 定义指向函数的指针
double (*funcTable)(double, double);
// main
funcTable = add;
funcTable(1.0, 2.0); // 3.0000
注意:
- 指向函数的指针 声明必须和原函数的声明保持一致
- 指向函数的指针 指针的赋值必须在 main 函数中
无类型的指针
无类型的指针其实就是 void *
它是通用性指针。任何类型的指针都可以转换成 void *
类型,并且在将它转换回原来的类型时不会丢失信息
使用时,我们只需把它当做”接受任何类型的指针“来使用即可。但我们不能使用它来运算等操作。
例子:
double a = 1;
void *p;
double *d;
// 从 void 转换成 double * 类型的指针
p = &a;
d = (double *)p;
printf("%f", *d); // 1.0000
可见信息并没有丢失信息。
我们在指向函数的指针声明中用上 void *
double add( void *x, void *y ) { return *(double *)x + *(double *)y; }
double (*funcTable)(void *, void *);
// main
funcTable = add;
double a = 1.0;
double b = 2.0;
funcTable((void *)&a , (void *)&b); // 3.0000
结构体
结构是一个或多个变量的集合,这些变量可能为不同的类型,为了处理的方便而将这些变量组织在一个名字之下
结构可以拷贝、赋值、传递给函数,函数也可以返回结构体。
快速了解
简单的声明:
struct point
{
int x;
int y;
};
这里的 point 我们称之为 结构标记 ,在结构体内的变量我们称之为 成员(member) 。
声明结构体与结构体变量:
struct point
{
int x;
int y;
} diagram, chart;
此处的 diagram 和 chart 就是结构体变量,他们的类型是 struct point 类型。
既然如此我们也可以使用这样的声明并定义:
struct point diagram = {320 , 210};
访问结构体变量成员:
diagram.x
例子:
struct point
{
int x;
int y;
} diagram = {123, 213};
// mian
printf("%d", diagram.x); // 123
我们甚至还可以结构体嵌套:
struct race
{
struct point p1;
struct point p2;
} diagram = {{1,3}, {4,6}};
// main
printf("%d", diagram.p1.x); // 1
结构体指针
如果传递给函数的结构很大,使用指针的方式效率通常比复制整个结构的效率要高。
声明与普通变量一致:
struct point *pt;
使用:
(*pt).x
可能有人认为不是 *pt.x
吗?这里注意了,这里的括号是必须的。因为 . 运算符比 * 运算符优先级高。所以 *pt.x
= *(pt.x)
,这里 pt.x 并不是指针,使用 * 则是非法的。
结构体指针的使用频率很高,所以除了上面这种写法,还有另一种:
pt->x
结构数组
就是定义多个结构体实例,存放在数组中。
struct point
{
int x;
int y;
char id;
} diagram[] = {
{213, 341, 'guangdo'},
{435, 453, 'shanghai'},
{355432, 3241, 'shenzhen'}
};
值得注意的是:struct 的长度并不是各成员的长度之和。因为不同的对象有不同的对齐要求,所以结构体中可能会出现未命名的 ”空穴“(hole)
这个意思可以用上面的结构体 struct point 来看。这个结构体看似是 4 + 4 + 1 = 9 个字节。实际上是 4 + 4 + 4 = 12 个字节。
结尾
此上已是全部啦。 ❤️
参考资料:
- C程序设计语言(第2版.新版) (典藏版) [美]布莱恩·克尼汉(Brian W.Kernighan)丹尼斯·里奇(Dennis M.Ritchie)
- 中国慕课 猴博士课程 《四小时入门C语言》
如果有疑问请在下方评论哦。