C基础知识总结(全)
目录
第一个程序hello world说明
#include <stdio.h>
//#include 表示要包含的头文件
//头文件中一般存的都是函数的声明、类型的定义等
// stdio.h 是标准输入输出的头文件 printf函数的声明就在其中
// int 是函数的返回值类型
// main 是主函数 是程序的入口,每个程序有且仅有一个main函数
// ()里面是main函数的参数列表,可以空着,但()需要写
// {}里面是函数体,也是实际要执行的代码
// int main(){}
int main(int argc, const char *argv[])
{
// printf 是一个输出函数 功能:将内容输出(打印)到终端
// "" 里面的就是要打印的内容 n 表示换行 相当于回车
//注意:c语言中 每条指令结束 都要一个 分号;
printf("hello worldn");
//函数的返回值
return 0;
}
//单行注释
/*
多行
注释
*/
计算机中的数据存储
计算机中数据的存储分为两大块:数值型数据的存储 非数值型数据的存储
数值型数据的存储
二进制(方便计算机处理的)
逢2进1 每一位上的数字只能是 0 或者 1
前导符:0b 如 0b10100011 0b11001010
十进制
逢10进1 每一位上的数字是 0~9
例如:520 1314
二进制转十进制
从右向左
101011 --> 1*2^0 + 1*2^1 + 0*2^2 + 1*2^3 + 0*2^4 + 1*2^5 == 43
其他进制转十进制都可以使用这种方式,只不过把底数的2 换成 对应进制的数即可。
二进制转十进制(用熟练了也可以使用8421方式转):
1000 8
0100 4
0010 2
0001 1
1100 1000+0100 = 8 + 4 = 12
十进制转二进制:
除2取余法
用十进制数除2,保存商和余数
在用上继续除2,再保存商和余数
直到商为0结束
最后,将得到的余数倒着取,就得了对应的二进制
八进制
逢8进1 每一位上的数字 0~7
前导符 0 如 0567 0123
八进制转二进制:
方法1:先将八进制转成十进制,然后十进制再转二进制
方法2:每1位八进制数,对应3位二进制数
0357 --> 011101111 --> 11101111
同理 二进制 转八进制
从右向左,每3位二进制数,对应1位八进制数,高位不足就补0
0b011011 --> 33
十六进制
逢16进1 a 10 b 11 c 12 d 13 e 14 f 15
每一位上的数据 0~9 a~f
前导符 0x 如:0x34 0x7f 0xcb
十六进制转二进制:
方法1:先将十六进制转成十进制,然后十进制再转二进制
方法2:每1位十六进制数,对应4位二进制数
0x7d ---> 01111101
同理 二进制 转十六进制
从右向左,每4位二进制数,对应1位十六进制数,高位不足就补0
0b01011011 --> 0x5b
注意:不管几进制的数,在计算机中,最终都会转变成二进制处理。
非数值型数据的存储
因为计算机只能处理二进制的数值型数据,但是在实际的开发过程中,
经常会出现一些非数值型的数据,比如人名、地名、企业名。。。
我们需要让计算机能识别这些数据,于是科学家们就发明一种叫做 ascii 码的东西,
在linux系统中 使用命令 man ascii 就可以查ascii码表,
如果说代码中出现了 使用 单引号 或者双引号引住的数据,都是非数值型数据
"hqyj" 'm' '7'
常见的ascii码
'' ---> 0
'n' ---> 10
'0'~'9' ---> 48~57
'A'~'Z' ---> 65~90
'a'~'z' ---> 97~122
转义字符:
所有的ascii码都可以使用 ''+一个八进制数来表示
c语言中定义了一些字母前面加上 '' 来表示一些不能显示的 ascii 码
如 'n' 't' '' 这些就称之为转义字符,因为后面的字母已经不是本身的含义了
词法符号
关键字
关键字是系统中已经定义好的一些词汇,直接使用即可。
char short int long float double enum struct union signed unsigned void --12个
auto const static volatile register extern --6个
typedef
sizeof
if else switch case default break do while for return continue goto --12个
注意:C语言是严格区分大小写的,关键字都是小写的。
标识符
所谓的标识符,就是自己在代码中起的名字,函数名、变量名、结构体名。。。等
自己起名时要符合标识符的命名规范:(起的名字尽量做到望文知义)
1.由数字、字母、下划线组成
2.不能以数字开头
3.不能和关键字冲突
数据类型
C语言的本质是 操作内存
内存和硬盘的区别:
内存:数据掉电丢失
硬盘:数据掉电不丢失
内存分配的最小单位 字节
1bit 一个比特位 只能存储一个1或者0
1Byte 一个字节 1Byte = 8bit
1KB 1024B
1MB 1024KB
1GB 1024MB
1TB 1024GB
数据类型的作用:相当于一个模子,规定了由他定义的变量占用的内存空间的大小。
数据类型的分类
基本类型:
整数类型、浮点类型(实型 小数)、枚举
构造类型:
数组、结构体、共用体
指针类型:
空类型
整数类型
整数类型由分为 char 、short 、int 、long 、 long long
其中,char 也叫字符类型:字符就是整型,整型就是字符,具体看我们怎么用。
每种类型有分为 有符号的(signed) 和 无符号的(unsigned)
默认的就是有符号的,对于有符号的数据 最高位 0 正数 1 负数
char 字符类型
char 类型定义的变量在内存中占用 1字节 8bit
存储的数据范围:
无符号的:0 ~ 255
有符号的:-128 ~ 127
注意:为了防止正负0的问题,计算机中存储的是数据的补码,
规定 10000000 是 -128 的补码
char类型多用于存储字符的ascii码,ascii码最大值是 127
short 短整型
short 类型定义的变量在内存中占用 2字节 16bit
存储的数据范围:
无符号的:0 ~ 2^16-1
有符号的:-2^15 ~ 2^15-1
int 整型
int 类型定义的变量在内存中占用 4字节 32bit
存储的数据范围:
无符号的:0 ~ 2^32-1
有符号的:-2^31 ~ 2^31-1
long 长整型 (也可以写成 long int)
在32位系统中 和 int 一样
在64位系统中 和 long long 一样
long long 长长整型(也可以写成 long long int)
long long 类型定义的变量在内存中占用 8字节 64bit
存储的数据范围:
无符号的:0 ~ 2^64-1
有符号的:-2^63 ~ 2^63-1
浮点类型(实型 小数)
float 类型
单精度浮点型,占用4字节
double 类型
双精度浮点型,占用8字节
long double 类型--了解就行
多精度浮点型,32位系统 12字节 64位系统 16字节
空类型
void 类型 是空类型 一般不单独使用 一般在指针中会用到
默认是不占用字节数的,具体和编译器有关
原码、反码、补码
数据在存储的时候,涉及 原码 反码 补码 转换的问题。
原码:方便人类识别的
反码:用来做原码和补码转换的
补码:是计算机中存放数据的形式
无符号数:不涉及负值,所以 原码 反码 补码 都是一样的
有符号正数:原码 反码 补码 都是一样的
有符号负数:
反码 = 原码中符号位不变,数据位按位取反(1变0 0变0)
补码 = 反码+1
方法:存储时看正负,取出时看类型(有无符号)
常量
概念
所谓的常量,就是在整个程序运行的过程中,值不会发生变化的量。
一般多用于给变量赋值。
分类
前导符 例如 输出占位符
整型常量
二进制 0b 0b1010 无
八进制 0 0567 %o
十进制 无 1314 %d
十六进制 0x 0x34cd %x
实型常量
float 3.14 %f
double 5.28 %lf
字符常量 'H' %c
字符串常量 "HQYJ" %s
指数常量(类似于科学计数法) %e
标识常量(宏定义) --define
实型常量
实型又称为浮点型,一般都是含有小数部分的。
在C语言中,只有十进制的实数,又分为单精度的(float) 和 双精度的(double)
浮点型存储涉及到小数的二进制,实际存储的是一个拼凑的近似值。
表示方式有两种:
一般形式: 3.14 5.28 7.89
float %f double %lf
指数形式:
[+/-]M.Ne[+/-]T 例如 -3.14e2 相当于 -3.14*10^2 == -314
#include <stdio.h>
int main(int argc, const char *argv[])
{
//printf函数默认会显示小数点后6位
//超过的部分会 四舍五入
//不足6位小数 会用0 补满6位显示
float f1 = 3.141592653;
printf("f1 = %fn", f1);
printf("f1 = %.3fn", f1);//可以使用 %.nf 来指定显示 n 位小数
float f2 = 3.14;
printf("f2 = %fn", f2);
//double 类型要用 %lf 输出
double d1 = 3.141592653;
printf("d1 = %lfn", d1);
//也可以使用 %e 按指数形式输出
double d2 = -31459;
printf("d2 = %en", d2);
float f3 = -3.1459e+4;//指数常量
printf("f3 = %fn", f3);
return 0;
}
代码结果:
字符常量
字符:就是我们前面说的 一个 非数值型数据
格式:字符常量 必须使用 单引号 '' 引起来 且一个单引号里只能引一个字符
'm' 'q' '8' 'n'
字符就是整型,整型就是字符,具体看我们怎么用。
想输出字符---%c
想输出ascii码---%d
#include <stdio.h>
int main(int argc, const char *argv[])
{
//定义了一个char 类型的变量 名字叫c1 存储的字符 是 'H'
char c1 = 'H';
printf("c1 = [%d] [%c]n", c1, c1);//[72] [H]
char c2 = c1+2;
printf("c2 = [%d] [%c]n", c2, c2);//[74] [J]
char c3 = 10;
printf("c3 = [%d] [%c]n", c3, c3);//[10] [
//]
//如何将 字符的 '8' 转换成 整数的 8
//'8' 8
char num = '8';
//num = num-48;
num = num-'0';
printf("num = %dn", num);
//如何将 字符 'h' 转换成 'H'
char num2 = 'h';
//num2 = num2 - 32;
num2 = num2 - ('a'-'A');
printf("num2 = %cn", num2);//H
return 0;
}
字符串常量
字符串是由一个或多个组成
格式:字符串常量需要用 双引号 "" 引起来
如 "www.baidu.com"
在C语言中,每个字符串结尾都会有一个隐藏的 '' 来标识字符串结束
注意:
a 变量
'a' 字符常量
"a" 字符串常量
输出字符串,使用 %s 作为占位符
标识常量(宏定义)
宏定义,就给表达式指定一个名字,使用这个名字,就是使用这表达式
是防止魔鬼数字的一种方式。
宏定义是在预处理阶段完成替换的,会将代码中所有使用宏名的地方,都用宏值替换。
也就是说,一旦宏定义的值变了,代码中所有使用他的位置都会变。
格式: #define 宏名 宏值
其中 宏名 是一个标识符,要符和标识符的命名规范,一般情况下,宏名都大写。
注意:宏定义是一个简单的 无脑的替换。
变量
概念
程序运行的过程中,值允许发生变化的量。
变量一般多用于存储数据。
变量的大小:只取决于定义变量时的类型
变量名也是一个标识符,要符合命名规范
定义变量的格式:
存储类型 数据类型 变量名;
存储类型:const static extern volatile register auto
局部变量不写存储类型默认的都是 auto
变量的初始化和赋值
#include <stdio.h>
int main(int argc, const char *argv[])
{
//在定义变量的同时 给他一个初始值
//这种方式就叫做变量的初始化
int value1 = 100;
printf("value1 = %dn", value1);//100
//先定义变量,然后使用时再给他一个值
//这种方式叫做变量的赋值
int value2;
printf("value2 = %dn", value2);//随机值
value2 = 520;
printf("value2 = %dn", value2);//520
//变量如果没有初始化,里面存的就是随机值
//有些时候随机值 会影响到我们,所以一般,不知道用什么值
//给变量初始化时 可以用0初始化
int value3 = 0;
//可以使用常量给变量赋值
//也可以使用变量给变量赋值
value3 = value2;
printf("value3 = %dn", value3);//520
//变量也可以参与运算
int result = value1 + value2;//只要不给变量赋值,运算就不会影响到变量原来的值
//此处 value1 和 value2 的值不会变
printf("result = %d value1 = %d value2 = %dn", result, value1, value2);// 620 100 520
//同一个作用域(同一个{})内部,不允许定义重名的变量
//int value3 = 123;
//可以在一行里定义多个变量 中间用逗号分隔
int a = 10, b = 10, c = 20, d = 30;
printf("a = %d, b = %d, c = %d, d = %dn", a, b, c, d);// 10 10 20 30
return 0;
}
强制类型转换
强制类型转换(简称 强转),就是通过某种方式,将表达式的值在某次运算的过程中,
转换成其他类型来参与运算
显式的强转
格式 : (新的类型名)表达式;
强制类型转换是不安全的,要谨慎使用。
如果是小的类型转换成大的类型,一般没什么问题,
但是大的类型转换成小的类型,就有可能出现数据的丢失。
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a = 5;
int b = 2;
int c = a/b;
printf("c = %dn", c);//结果是2
//因为a和b是int 类型 计算的结果会舍弃小数位
//注意:只是在本次运算中,a和b的值转换成float参与运算
//a和b本身还是 int 类型
float d = (float)a/(float)b;
printf("d = %fn", d);//2.500000
return 0;
}
隐式强转
由编译器根据上下文自动推导,隐式的做强制类型转换。
对于不安全的强转,编译器可能会报出一个警告,
有些编译器不报警告,具体取决于编译器的严谨程度。
#include <stdio.h>
int main()
{
float f = 3.14;
int value = f;//相当于取整操作 会舍弃小数位
//int 存 float的整数位一定存的下 是安全的
printf("f = %f value = %dn", f, value);//3.14 3
return 0;
}
注意:
有符号数和无符号数运算时,会将有符号数强转成无符号数参与运算
运算的结果是一个无符号数。
#include <stdio.h>
int main(int argc, const char *argv[])
{
unsigned char a = 6;
signed char b = -20;
if(a+b > (unsigned)0){
printf("yesn");//此处代码会执行,因为 a+b 表达式的结果是大于0的
}
return 0;
}
运算符
概念
运算符就是一个符号,用于多个值之间做运算
运算符是用来连接表达式的
所谓的表达式:就是由运算量、运算符和标点符号等组成的一个有效序列,
能用来描述一个运算的过程。
分类
按照类型区分:
算数运算符: + - * / %(模除 取余数) ++ -- (模除运算要求,左右操作数必须是整数。)
关系运算符: > < >=
逻辑运算符: && || !
位运算符: & | ^ ~ >
赋值运算符: = += -= ...复合赋值运算符
条件运算符: ?:
sizeof运算符 :计算变量或者类型占用的内存空间的大小的
逗号运算符: (表达式1,表达式2,...,表达式n) (一般不用)
++ -- 运算符
a++ <==>a = a+1
++a <==>a = a+1
a-- <==>a = a-1
--a <==>a = a-1
以a++和++a为例:
两种操作 a 的值都 +1 了,但是表达式的结果是不一样的
a = 10;
b = ++a;
上述两步操作之后 a = 11 b = 11
a = 10;
b = a++;
上述两步操作之后 a = 11 b = 10
a--和 --a 同理。
关系运算符> < >= <= == !=
关系运算符就是用来判断多个表达式之间的关系的,
关系运算符的表达式的结果是一个 布尔类型
布尔类型: 0 假 非0 真
一定要注意: == 和 = 的区别
== 是关系运算符的判断相等
= 是赋值运算符
逻辑运算符
逻辑运算符一般多用于连接多个由关系运算符组成的表达式
所以逻辑运算符的表达式的结果也是一个布尔类型。
&& 逻辑与 表示并且的意思
逻辑与连接的多个表达式全为真,整个表达式的结果才为真
有一个表达式为假,则整个表达式的结果就为假
|| 逻辑或 表示或者的意思
逻辑或连接的多个表达式,有一个为真,则整个表达式就为真
所有的表达式都为假,整个表达式才为假
! 逻辑非 表示逻辑取反的意思
真变假 假变真
注意:下面的用法C语言不支持
10 < x < 20
C语言需要写成 (x>10 && x < 20)
逻辑运算符的短路原则:
逻辑与连接的多个表达式,如果有一个为假,则后面的表达式就都不执行了
逻辑或连接的多个表达式,如果有一个为真,则后面的表达式就都不执行了
位运算符
位运算符是相对于二进制而言的,所以但凡涉及到位运算,计算机都会先将数据转换成
二进制,再参与运算,且一般位运算使用的都是无符号的数据,如果用有符号的可能会
设计原码、反码、补码相互转换的问题。
位运算一般多用于硬件设备的控制,及对标志位等的控制。
& 按位与 按位运算 全1为1 有0为0
| 按位或 按位运算 有1为1 全0为0
~ 按位取反 按位运算 1变0 0变1
^ 按位异或 不同为1 相同为0
<< 左移运算 按位左移 低位补0 舍弃高位
>> 右移运算 按位右移 高位补0 舍弃低位
位运算操作小技巧:
1或任何数 都是1
1与任何数 还是任何数
0或任何数 还是任何数
0与任何数 都是0
赋值运算符 =
一般形式
左边 = 右边;
注意
是将右边的值赋给左边
左边一定是一个变量,不能是常量
注意
= 和 == 区别
= 赋值
== 关系运算符 判断相等
复合赋值运算符
+= -= *= /= 等
a+=b <==> a = a+b;
a-=b <==> a = a-b;
详细的参考下图:
条件运算符 ?:
也是C语言中唯一一个三目运算符。 有三个操作
表达式1 ? 表达式2 : 表达式3;
执行逻辑:
如果表达式1为真,则执行表达式2,否则执行表达式3
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
int c = 0;
c = (a>b ? ++a : ++b);
printf("a = %d b = %d c = %dn", a, b, c);//10 21 21
return 0;
}
sizeof运算符
sizeof运算符是用来计算变量或者类型的大小的。
单位是 字节。
注意sizeof的用法和函数调用很像,但是他是一个运算符 而不是函数
格式:
sizeof(变量名或者类型名)
#include <stdio.h>
int main(int argc, const char *argv[])
{
char a;
short b;
int c;
long d;
long long e;
float f;
double g;
//sizeof的结果可以用变量保存
int ret = sizeof(int);
//也可以直接输出 64位系统 用 %ld 输出 32位系统用%d输出
printf("sizeof(char) = %ld sizeof(a) = %ldn", sizeof(char), sizeof(a));//1
printf("sizeof(short) = %ld sizeof(b) = %ldn", sizeof(short), sizeof(b));//2
printf("sizeof(int) = %ld sizeof(c) = %ldn", sizeof(int), sizeof(c));//4
printf("sizeof(long) = %ld sizeof(d) = %ldn", sizeof(long), sizeof(d));//8
printf("sizeof(long long) = %ld sizeof(e) = %ldn", sizeof(long long), sizeof(e));//8
printf("sizeof(float) = %ld sizeof(f) = %ldn", sizeof(float), sizeof(f));//4
printf("sizeof(double) = %ld sizeof(g) = %ldn", sizeof(double), sizeof(g));//8
return 0;
}
运算符的优先级
如果搞不清楚优先级,最好的方法是加括号 () 只要加的位置合理,加多了不算错
优先级顺口溜:
单算移关与,异或逻条赋。
程序结构
C语言的程序结构只有三种
顺序结构
分支结构(选择结构)
循环结构
分支结构
if...else语句
1.简化格式
if(表达式){
代码块;//代码块允许有多行
}
执行逻辑:如果表达式为真 则执行代码块,如果为假,就不执行代码块
if(表达式){
代码块1;
}else{
代码块2;
}
执行逻辑:如果表达式为真 则执行代码块1,
如果为假,则执行代码块2
2.阶梯格式
if(表达式1){
代码块1;
}else if(表达式2){
代码块2;
}else if(表达式n){
代码块n;
}else{
其他分支;
}
执行逻辑:
如果表达式1为真,则执行代码块1,
否则继续判断表达式2,如果为真,则执行代码块2
否则继续向下判断,如果给的条件都不满足
就走其他分支
其中整个else模块可以没有,表示不关系其他分支的情况
3.嵌套格式
执行逻辑和前面的一样,只不过是if..else 的嵌套使用
if(表达式1){
if(表达式11){
代码块11;
}else if(表达式12){
代码块12;
}else{
其他分支;
}
}else if(表达式2){
if(表达式21){
代码块21;
}else if(表达式22){
代码块22;
}else{
其他分支;
}
}else{
其他分支
}
例如:
#include <stdio.h>
int main(int argc, const char *argv[])
{
printf("----start----n");
int num = 0;
scanf("%d", &num);
//简化格式
#if 0
if(num>0){
printf("yesn");
}
#endif
#if 0
if(num>0){
printf("yesn");
}else{
printf("non");
}
#endif
//阶梯格式
#if 0
if(num>0 && num <=10){
printf("0<num<=10n");
}else if(num > 10 && num<=20){
printf("10<num<=20n");
}else{
printf("othern");
}
#endif
//嵌套格式
if(0<=num && num<=100){
if(0<=num && num<=50){
printf("0~50n");
}else{
printf("51~100n");
}
}else if(100<num && num<=200){
printf("100~200n");
}else{
printf("othern");
}
printf("----end----n");
return 0;
}
注意事项:
1.if或者else if或者else后面的语句只有一行时,{}可以不写
但是注意,只要超过一行 就必须要加{}了
2.else不能独立出现,前面必须有if与之对应,否则报错
3.else和同一层次的前面与之最近的if结合
4.表达式一般是一个由关系运算符和逻辑运算符组成的表达式,用来判断真假的
bool 类型 0假 非0真
如果表达式不是一个正常的,要注意下面的用法:
if(a=b) //表达式的结果和b有关 b为0 假 b为非0 真
if(a) //表达式的结果和a有关 a为0 假 a为非0 真 等价于 if(a!=0)
if(!a) //表达式的结果和a有关 a为0 真 a为非0 假 等价于 if(a==0)
5.当同一层次中出现两个if与时
if(xx){
xxx
}
if(xx){
xxx
}
上面两个if语句 相互之间是独立的 互不影响
6.分支控制语句,只会选择一条分支执行或者每条分支都不选。
switch..case 语句
switch(表达式){
case 常量表达式1:
代码块1;
break;
case 常量表达式2:
代码块2;
break;
case 常量表达式n:
代码块n;
break;
default:
其他分支;
break;
}
例如:
#include <stdio.h>
int main(int argc, const char *argv[])
{
int num = 0;
scanf("%d", &num);
switch(num){
case 10://如果是字符类型 需要加 单引号
printf("1111111111n");
//break;//如果没有break 程序会继续执行下面分支的代码块
case 20:
printf("222222222222n");
break;
case 30:
printf("3333333333n");
break;
default://如果不关心其他分支 整个default 分支可以不要
printf("othern");
break;
}
return 0;
}
注意事项
1.switch后面()里的表达式一般是一个变量,或者变量表达式
case 后面的常量表达式就是这个变量的可能的结果
2.case后面的常量表达式一般是一个整型或者字符型,不能是浮点型
3.关于default分支,如果前面的所有case都不满足,就会统一交给default分支处理
相当于if..else语句中的 else分支,如果不需要,可以不写。
4.每个分支结束,后面都要加上break,表示执行完该分支后结束整个switch..case 语句
如果不加break,程序会继续执行下面分支的语句,直到遇到break或switch..case语句结束
这种现象称之为:case击穿
循环控制语句
使用goto实现循环
说明
goto本来是用来做跳转的,只能在同一个函数中进行跳转。
goto的强制跳转对代码的可读性和逻辑性会有一定的影响,所以要慎用goto。
基本用法
代码块1;
goto NEXT;
代码块2;
NEXT:
代码块3;
执行逻辑,先执行代码块1,然后遇到goto 直接跳转到对应的标签
然后运行代码块3, 代码块2就被跳过了
NEXT: 就是标签名,是一个标识符,符合命名规范即可,一般使用大写居多
循环语句
代码块1;
HQYJ:
代码块2;
if(!结束条件){
goto HYQJ;
}
代码块3;
例如:
#include <stdio.h>
int main(int argc, const char *argv[])
{
printf("1111n");
goto NEXT;
printf("2222n");//就不执行了
NEXT:
printf("3333n");
return 0;
}
#include <stdio.h>
int main(int argc, const char *argv[])
{
int count = 0;
printf("1111n");
NEXT:
printf("2222n");
count++;
if(count < 5){
goto NEXT;
}
printf("3333n");
return 0;
}
注意:除了特殊场景需要用到死循环,绝大部分情况下使用的循环都是有结束条件的。
例如:
自己测试时,防止写出死循环刷屏 可以在循环中适当的加 sleep(1) 让进程休眠1秒
需要加头文件 #include <unistd.h>
while 循环
while(表达式){
循环体;//就是要循环执行的代码
}
执行逻辑:
先执行表达式,如果表达式为真,则执行循环体,
然后在执行表达式,如果还为真,则继续执行循环体
直到表达式为假 循环立即结束 , 继续执行循环后面的代码
表达式的用法和 if 语句的表达式用法一样
注意:循环体中一般都有能改变表达式结果的语句
否则就是死循环了。
例如:使用while循环打印 5 行 hello world
#include <stdio.h>
int main(int argc, const char *argv[])
{
printf("--start--n");
int i = 0;//一般用来控制循环结束的变量我们称之为 循环变量
//循环变量 的名字一般使用 i j k 即可
while(i<5){
printf("hello worldn");
i++;//用来改变表达式的结果 从而控制循环的结束的条件
}
printf("--end--n");//没有被循环的 {} 包住的部分 不受循环的控制
return 0;
}
do..while 循环
do{
代码块;
}while(表达式);
注意:do..while后面要有一个分号
执行逻辑:
和while是类似的,只不过while是先判断后执行
而do..while是不管条件为真还是为假,代码块都先执行一次
例如:
#include <stdio.h>
int main(int argc, const char *argv[])
{
int i = 0;
do{
//不管while后面的条件为真还是为假
//代码块都会先执行一次
printf("hello worldn");
i++;
}while(i<5);
return 0;
}
for 循环
for(表达式1;表达式2;表达式3){
循环体;
}
执行逻辑:
先执行表达式1,然后执行表达式2,如果表达式2为真
则执行循环体,然后执行表达式3,然后再执行表达式2,
如果还为真,则继续执行代码块和表达式3
直到表达式2为假 循环立即结束 继续执行循环后面的代码
表达式1:只执行一次 一般是用来给变量赋初值的
表达式2:和if while的表达式是一样的,是用来判断真假的
表达式3:一般是用来修改表达式1中的变量的值,从而控制循环结束的
// for 与 while 对比
//打印5行hello world
//while
int i = 0;
while(i<5){
printf("hello wroldn");
i++;
}
//for
int i = 0;
for(i = 0; i < 5; i++){
printf("hello wroldn");
}
死循环
有些场景我们需要让程序一直运行,如后台的服务器程序等
就需要用到死循环 ---无限制的一直执行 直到我们手动让他退出
while(1){ //一般使用这种用法的居多
//代码块
}
for(;;){ //表达式可以不写 但是两个分号必须写
//代码块
}
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
#if 0
while(1){
printf("hello worldn");
sleep(1);
}
#endif
for(;;){
printf("hello worldn");
sleep(1);
}
return 0;
}
C语言中求n次方和开方的函数
注意:math是三方库,不是系统提供的,所以编译时要链接库
需要加编译选项 -lm
pow函数
#include <math.h>
double pow(double x, double y);
功能:计算x的y次幂
返回值就是计算的结果
sqrt函数
#include <math.h>
double sqrt(double x);
功能: 计算x的非负平方根
返回值就是计算的结果
#include <stdio.h>
#include <math.h>
int main(int argc, const char *argv[])
{
int a = 2;
int b = 3;
float ret = pow(a, b);
printf("ret = %fn", ret);//8
ret = sqrt(16);
printf("ret = %fn", ret);//4
return 0;
}
辅助控制关键字
break
break关键字可以用在 switch..case 语句中,表示执行完某一分支就结束整个switch..case语句
break也可以用在循环中,表示立即结束本层循环
注意:break只能用在这两处,用在其他位置会报错。
continue
continue只能用在循环中,表示结束本层的本次循环。
return
return 一般是用在子函数中,表示结束函数调用并返回函数运算的结果。
如果return用在主函数中,表示结束整个程序,后面的代码都不执行了。
#include <stdio.h>
int main(int argc, const char *argv[])
{
int i = 0;
int j = 0;
for(i = 0; i < 5; i++){
for(j = 0; j < 5; j++){
if(j==2){
//break;
//continue;
return 0;
}
printf("%d--%dn", i, j);
}
}
return 0;
}
数组
概念
数组是用来管理一组相同数据类型的数据的
数组是一个构造类型
数组中保存的每个数据称为数组的元素或者数组的成员
数组在内存中需要分配一块连续的空间,不管几维数组,在内存上都是连续的
一维数组
概念
所谓的一维数组,就是下标只有一个的数组。一维数组的成员在内存上是连续的。
定义一维数组
存储类型 数据类型 数组名[下标];
存储类型:不写默认的就是 auto
数据类型:数组中每个元素的类型,可以是基本类型、也可以数构造类型(数组除外)
数组名: 是一个标识符,要符合标识符的命名规范
下标: 在定义数组的时候,下标表示要定义的数组的长度,一般都是用常量
在其他场景下,下标都表示访问数组中的第几个元素,
可以是常量、也可以是变量、也可以是表达式
如: int s[10];
表示定义了一个数组,数组名为s 数组中的每个元素都是一个int ,数组中共有10个元素。
一维数组的性质
#include <stdio.h>
int main(int argc, const char *argv[])
{
int s[5];
//定义一个一维数组 数组名叫s
//数组中有5个元素 每个元素都是一个int类型
//一维数组成员的访问
//访问方式 数组名[下标] 下标是从0开始的
//当取出一个成员后 使用方法 和一个int类型的变量的使用方法是一样的
s[0] = 10;
s[1] = 20;
s[2] = 30;
printf("s[0] = %dn", s[0]);//10
printf("s[1] = %dn", s[1]);//20
printf("s[2] = %dn", s[2]);//30
printf("s[3] = %dn", s[3]);//随机值
printf("s[4] = %dn", s[4]);//随机值
//数组的成员在内存上是连续的
//printf %p 打印变量的地址
// & 取变量的地址
//printf("%p", &变量);
printf("&s[0] = %pn", &s[0]);//依次相差一个int
printf("&s[1] = %pn", &s[1]);
printf("&s[2] = %pn", &s[2]);
printf("&s[3] = %pn", &s[3]);
printf("&s[4] = %pn", &s[4]);
//一维数组的数组名是一个常量
//不能被赋值 也不能执行 ++ 等操作
//s = 10;//错误的
//s++;//错误的
//一维数组的大小 = 数组中一个元素的大小 * 元素的个数
printf("sizeof(s) = %ldn", sizeof(s));//20
//当数组定义完成后,就不能整体赋值了
//只能一个元素一个元素的赋值
s[0] = 520;
s[1] = 1314;
//s = {520, 1314};//错误的
//一维数组成员的遍历
int i = 0;
//for(i = 0; i < 5; i++){
for(i = 0; i < sizeof(s)/sizeof(s[0]); i++){
printf("%d ", s[i]);
}
printf("n");
return 0;
}
一维数组的初始化
#include <stdio.h>
int main(int argc, const char *argv[])
{
//一维数组如果没有初始化 那么成员都是随机值
//int s[5];
//一维数组的初始化
//方式1:完全初始化
//int s[5] = {10, 20, 30, 40, 50};
//方式2:不完全初始化
//注意:这种方式 是下标从小到大依次初始化 没有初始化的位 默认用0初始化
//int s[5] = {10, 20, 30};
//方式3:全都初始化成0 --常用
//int s[5] = {0};
//方式3:省略下标的初始化 这种方式 操作系统会根据
//给定的元素的个数自动计算需要的空间的大小
int s[] = {10, 20, 30, 40, 50};
//数组一旦初始化了,就不能再整体赋值了
//s = {1,2,3,4,5,};//错误的
//一维数组的遍历
int i = 0;
for(i = 0; i < 5; i++){
printf("%d ", s[i]);
}
putchar(10);
//注意!!!!!!!!!
//访问数组时,编译器不会检查数组越界的错误,数组越界需要
//程序员自己控制,切记,不能越界访问
//s[5] = 1314;//就已经是非法访问了 编译不报错 运行时错误不可预知
return 0;
}
冒泡排序
升序:从小到大
降序:从大到小
基本思想
相邻的两个元素之间进行比较,按照要求(升序还是降序)进行交换。
时间复杂度 是 O(n^2)
实现流程
以升序为例:
先进行第一趟排序,将第一个元素和第二个元素进行比较,将较大的放在第二个位置上,然后
第二个元素和第三个元素做比较,将较大的放在第三个位置上,依此类推,第一趟排序结束时,
最大的元素就在最后一个位置上了。
然后进行第二趟排序,将第一个元素和第二个元素进行比较,将较大的放在第二个位置上,然后
第二个元素和第三个元素做比较,将较大的放在第三个位置上,依此类推,第二趟排序结束时,
第二大的元素就在倒数第二个位置上了。
整个过程依此类推,直到最后一趟排序结束,整个数据就是有序的了。
冒泡排序动图:
代码实现:
#include <stdio.h>
int main(){
int s[10] = {1, 5, 3, 67, 53, 78, 900, 520, 1314, 666};
//排序前
int i = 0;
for(i = 0; i < 10; i++){
printf("%d ", s[i]);
}
printf("n");
#if 0
//先完成一趟排序
int temp = 0;
int len = sizeof(s)/sizeof(s[0]);
for(i = 0; i < len-1; i++){
if(s[i] > s[i+1]){
//交换
temp = s[i];
s[i] = s[i+1];
s[i+1] = temp;
}
}
#endif
//整个排序的流程
int temp = 0;
int len = sizeof(s)/sizeof(s[0]);
int j = 0;
//外层循环控制比较的趟数
// len-1 是因为 最后一趟只剩一个元素了就不用排序了
for(j = 0; j < len-1; j++){
//内层循环控制一趟排序比较的次数
//因为每趟都能确定一个最值,最值在下一趟中就不用
//再参与比较了,所以此处 len-1-j
for(i = 0; i < len-1-j; i++){
if(s[i] > s[i+1]){//如果是降序,只需要将此处的 > 改成 < 即可
//交换
temp = s[i];
s[i] = s[i+1];
s[i+1] = temp;
}
}
}
//排序后
for(i = 0; i < 10; i++){
printf("%d ", s[i]);
}
printf("n");
return 0;
}
二维数组
概念
所谓的二维数组,就是有两个下标的数组。
定义二维数组的格式
存储类型 数据类型 数组名[行数][列数];
如:
int s[3][4];
表示定义了一个二维数组,数组名叫s 数组元素都是int 类型
数组有3行4列 共计 12 个元素
注意:
虽然二维数组有行号和列号,但是本质都是一维数组(在内存上还是连续的)
其中列宽,决定了数组按行偏移时的跨度,也就是说列宽决定了二维数组的操作空间
示意图:
二维数组的性质
#include <stdio.h>
int main(int argc, const char *argv[])
{
int s[2][4];
//定义了一个二维数组,数组名叫s 数组元素都是int 类型
//数组有2行4列 共计 8 个元素
//二维数组访问成员
//数组名[行号][列号] 注意 行号和列号都是从0开始的
s[0][0] = 520;
s[0][1] = 1314;
printf("s[0][0] = %dn", s[0][0]);//520
printf("s[0][1] = %dn", s[0][1]);//1314
printf("s[1][1] = %dn", s[1][1]);//随机值
//二维数组的数组名也是一个常量
//不能被赋值 也不能++
//s = 123; //错误的
//s++; //错误的
//二维数组的成员在内存上也是连续的
printf("&s[0][0] = %pn", &s[0][0]);//依次相差4字节
printf("&s[0][1] = %pn", &s[0][1]);
printf("&s[0][2] = %pn", &s[0][2]);
printf("&s[0][3] = %pn", &s[0][3]);
printf("&s[1][0] = %pn", &s[1][0]);
printf("&s[1][1] = %pn", &s[1][1]);
printf("&s[1][2] = %pn", &s[1][2]);
printf("&s[1][3] = %pn", &s[1][3]);
//二维数组大小 = 行数 * 列数 * 一个元素的大小
printf("sizeof(s) = %ldn", sizeof(s));//32
//二维数组的遍历
int i = 0;
int j = 0;
//外层循环控制行数
for(i = 0; i < 3; i++){
//内层循环控制列数
for(j = 0; j < 4; j++){
printf("%d ", s[i][j]);
}
printf("n");
}
return 0;
}
二维数组的初始化
#include <stdio.h>
int main(int argc, const char *argv[])
{
//二维数组如果没有初始化,成员也都是随机值
//int s[3][4];
//二维数组初始化的方式
//以行为单位:
//完全初始化
//int s[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
//不完全初始化 没有初始化的位 默认用0初始化
//int s[3][4] = {{1,2},{5},{9,10,11}};
//不以行为单位:
//完全初始化
//int s[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
//不完全初始化 没有初始化的位 默认用0初始化
//int s[3][4] = {1,2,3,4,5};
//全部初始化成0 --常用
//int s[3][4] = {0};
//省略行数的初始化
//不够一行时,也会分配一整行的空间
//列数一定不能省略 因为列宽决定了数组按行偏移时的跨度
int s[][4] = {1,2,3,4,5,6,7,8,9};
printf("sizeof(s) = %ldn", sizeof(s));//48
//二维数组的遍历
int i = 0;
int j = 0;
for(i = 0; i < 3; i++){
for(j = 0; j < 4; j++){
printf("%d ",s[i][j]);
}
putchar(10);
}
return 0;
}
字符数组和字符串
#include <stdio.h>
int main(int argc, const char *argv[])
{
//字符数组就是 char 类型的数组,每个元素都是一个char类型的变量
char s1[5] = {'a','b','c','d','e'};
s1[0] = 'A';
s1[2] = 66;
int i = 0;
for(i = 0; i < 5; i++){
printf("%c", s1[i]);
}
putchar(10);
//可以将字符串存在字符数组中
//注意:使用字符数组存储字符串时,要多给 '' 留处一个字节的空间
char s2[5] = {"QWER"};
char s3[5] = "hqyj";
//这种用法也可以,操作系统分配空间时会将 ''算进去
char s5[] = "www.hqyj.com";
printf("sizeof(s5) = %ldn", sizeof(s5));//13
//这种用法是错误的 因为 '' 会越界 错误就不可预知
//char s4[4] = "hqyj";
printf("%sn", s2);
printf("%sn", s3);
printf("%sn", s5);
//注意:printf使用 %s 输出字符串时,本质是从指定的首地址开始
//一直往后找 找到 '' 才结束 ,所以,如果不是字符串就不要使用 %s 输出
//C语言中对字符串的处理基本上都是 找首地址和 ''
printf("%sn", s1);//结果不可预知
return 0;
}
字符串处理函数
字符串处理函数有很多个,都在 string.h 中声明的
使用时都需要加上头文件 #include <string.h>
strlen() strcpy() strcat() strcmp()
strlen()
功能:
计算字符串的长度(不包括 '')
函数原型:
size_t strlen(const char *s);
参数:
s:要计算长度的字符串
返回值:
计算的结果
strcpy()
功能:
字符串的复制,将src复制到dest里面。
注意,要保证dest足够大,否则可能出现数组越界访问。
函数原型:
char *strcpy(char *dest, const char *src);
参数:
src:源字符串
dest: 目的字符串
返回值:
dest
strcat()
功能:
字符串的追加,将src追加到dest后面。dest的'' 会被覆盖
注意,要保证dest足够大,否则可能出现数组越界访问。
函数原型:
char *strcat(char *dest, const char *src);
参数:
src:源字符串
dest: 目的字符串
返回值:
dest
strcmp()
功能:
比较两个字符串
逐个比较两个字符串对应字符的ascii码
只要出现大小关系就立即返回,
如果,直到两个字符串都到第一个''了,前面的都相等 才认为两个字符串相等
函数原型:
int strcmp(const char *s1, const char *s2);
参数:
s1 和 s2 就是参与比较的两个字符串
返回值:
>0 s1>s2
==0 s1==s2
<0 s1<s2
指针
概念
内存中每个字节的空间都会有一个编号,这个编号就是指针
也叫作地址,保存指针的变量叫做指针变量。
只不过通常的情况下,我们常说的:
地址:说的是内存地址
指针:说的是指针变量
指针的相关操作
& : 获取变量的地址
* : 在定义变量时,*只起到一个标识作用,标识定义的是一个指针变量
其他场景下,取*操作都表示通过指针访问对应的地址上的数据
指针和变量的关系
指针的基本性质
#include <stdio.h>
int main(int argc, const char *argv[])
{
//内存中每个字节的空间都有一个编号,这个编号就是指针,也叫地址
//当程序执行到定义变量的语句时 操作系统会根据类型给变量分配空间
int a = 10;
//通过变量名可以获取到变量的地址
// & : 获取变量的地址 &a 取到的是变量的首地址 编号最小的那个
// %p 输出地址
printf("&a = %pn", &a);
//常量没有地址可言
//printf("&100 = %pn", &100);//错误的
//普通变量是可以 存储 变量的地址的
//但是一般 不会这么做
//因为 没法通过保存地址的变量取访问对应的内存空间
//下面例子 temp可以保存 a的地址 但是没法通过 temp 改变a的值
//long temp = &a;
//printf("temp = %pn", temp);
//*temp = 1314;//错误的 普通变量不允许做 * 操作
//定义一个指针变量p 保存 变量a的地址的
//也称为 指针p 指向 a
//格式 数据类型 *指针变量名;
int *p = &a;
printf("p = %pn", p);
//通过p是可以操作 a所在的内存空间的
*p = 1314;//*p 表示访问 p保存的地址中的数据
printf("a = %dn", a);
//指针保存了变量的地址后 有下面的关系
//p <==> &a
//a <==> *p
printf("p = %p &a = %pn", p, &a);
printf("*p = %d a = %dn", *p, a);
//指针变量也是一个变量 也需要操作系统分配空间
printf("&p = %pn", &p);
//指针只能保存已经分配给你的空间的地址
int *p2 = 0x123456;
//printf("p2 = %pn", p2); //可以保存 但是不能操作
//*p2 = 520;//错误的
//指针如果没有初始化 指向是不确定的 因为存的是随机值
//这种指针叫做 野指针
//野指针是有害的 错误不可预知
//int *p3;//注意:代码中不要出现野指针
//指针变量的初始化
// NULL 空地址 0地址 NULL 本质 (void *)0
int *p4 = NULL; //不确定要指向谁的指针先让他指向NULL
//这种指针叫空指针
//对空指针的操作 一定会段错误
//*p4 = 1314;//段错误
return 0;
}
指针的运算
指针的运算,本质就是,指针变量里面存的地址之间的运算。
所以,运算的操作就是有限的了
指针之间可以做
算数运算 + - ++ --
关系运算 > < >= <= != ==
赋值运算 =
同种类型的指针之间做运算才有意义,不同类型的指针做运算是没有意义的。
指针变量的大小
32位系统 指针都是4字节
64位系统 指针都是8字节
#include <stdio.h>
int main(int argc, const char *argv[])
{
//指针类型 的作用
//决定了从指针指向的编号开始,可以操作几个字节的空间
//一般为了让指针操作的范围和指向的变量操作的范围一致
//都使用和变量类型一致的指针
int num = 1314;
int *ppp = #
//对于两个变量而言 地址运算没有意义 因为地址都是有操作系统分的 是随机的
//对数组来说 指针的运算是有意义的 因为数组成员在内存上是连续的
int s[5] = {10,20,30,40,50};
int *p2 = &s[0];
int *p3 = &s[2];
//两个指针做减法 差值 是相差的 指针的 数据类型的个数
//而不是相差的字节数
printf("p3 - p2 = %ldn", p3 - p2);//2 相差两个 int
//指针的强制类型转换是 安全的
char *q1 = (char *)p2;
char *q2 = (char *)p3;
printf("q2 - q1 = %ldn", q2 - q1);//8
//一个指针+ 或着 - 一个整数n
//表示: + 或者 - n*sizeof(指针的类型) 个字节
int *p4 = p2+4;//相当于 加了4 个int
printf("*p4 = %dn", *p4);//50
//两个指针是可以做关系运算的 但是一般也是在数组成员的地址中
//比较大小才有意义 以大于为例 其他同理
if(p3 > p2){
printf("yesn");
}else{
printf("non");
}
printf("--------------------n");
int a = 10;
int b = 10;
int *hqyj1 = &a;
int *hqyj2 = &b;
if(hqyj1 == hqyj2){//比较的是 a和b的地址
printf("yes1n");
}
if(*hqyj1 == *hqyj2){//比较的是 a和b的值
printf("yes2n");
}
printf("--------------------n");
//指针变量也是变量 变量之间是可以相互赋值的
int *p5 = NULL;
p5 = p2;//正确的
p5 = p3;//正确的
//注意下面的用法
int s2[5] = {10,20,30,40,50};
int *p = &s2[0];
int ret1 = *++p;
printf("ret1 = %dn", ret1);//20 先算 ++p 然后对 ++p表达式的结果取 *操作
// ++p 表达式的结果 是+之后的值
printf("p = %p &s[1] = %pn", p, &s2[1]);//相等
p = &s2[0];
int ret2 = *p++;
printf("ret2 = %dn", ret2);//10 先算 p++ 然后对 p++ 表达式的结果取 *操作
// p++ 表达式的结果 是+之前的值
printf("p = %p &s[1] = %pn", p, &s2[1]);//相等
return 0;
}
指针和一维数组
#include <stdio.h>
int main(int argc, const char *argv[])
{
int s[6] = {10,20,30,40,50,60};
//分析 数组名[下标] 的方式访问成员 本质是
//以数组第0个元素为参考 向后偏移 取 *操作 取出的元素
//数组名是一个地址 只不过是一个地址常量
printf("s[0] = %dn", s[0]);//10
printf("*s = %dn", *s);//10
printf("*(s+0) = %dn", *(s+0));//10
printf("*(s+1) = %dn", *(s+1));//20
//下面两种写法是等价的
//*(s+i) <==> s[i]
*(s+3) = 520;
printf("s[3] = %dn", s[3]);
//定义一个指针 指向一维数组首地址 数组名就是首地址
//下面两种写法都可以
int *p = s; //----常用
//int *p = &s[0];
//注意:单独从数值来看 s &s[0] &s 是一样的
//但是 记住任何时候不要对数组名取地址 相当于给数组升维了
//指针指向一维数组后 有如下的等价关系
//*(s+i) <==> s[i] <==> *(p+i) <==> p[i]
//通过指针也可以修改数组的成员
*(p+4) = 1314;
printf("%d %d %dn", s[4], *(s+4), p[4]);
//p 和 s 的区别
//s是数组名 是常量 不改变指向 也不能 ++
//p是变量 可以改变指向 也可以进行 ++ 操作
p = &s[0];
printf("p = %p &s[0] = %pn", p, &s[0]);
p++;
printf("p = %p &s[1] = %pn", p, &s[1]);
//一维数组的遍历
int i = 0;
for(i = 0; i < 6; i++){
//printf("%d ", s[i]);
//printf("%d ", *(s+i));
//printf("%d ", p[i]);
printf("%d ", *(p+i));
}
putchar(10);
return 0;
}
指针和二维数组
#include <stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
//分析一下二维数组数组名操作的空间
//s : 二维数组数组名 他是一个行指针 操作的空间是一行元素
printf("s = %pn", s);
printf("s+1 = %pn", s+1);
//*s : 对二维数组数组名 取 *操作 相当于对指针的降维
//将行指针降维成列指针
//*s 就是一个列指针了
//s+i 表示偏移几行
//(s+i) 和 *(s+i) 的值是一样的都是 地址
//只不过类型不样 (s+i) 表示第i行的首地址 *(s+i) 第i行第0个元素的地址
//也就是说 得取两次 * 才能取道 元素的值
//s[i][j] <==> *(s[i]+j) <==> *(*(s+i)+j)
//过程
//s+i 是 第i行的地址
//*(s+i) 是第i行第0个元素的地址
//*(s+i)+j 是第i行第j个元素的地址
//*(*(s+i)+j) 第i行第j个元素
printf("s = %pn", s);
printf("*s = %pn", *s);
printf("*(s+0) = %pn", *(s+0));
printf("*(s+1) = %pn", *(s+1));
printf("**s = %dn", **s);
printf("**(s+1) = %dn", **(s+1));
printf("*(*(s+1)+0) = %dn", *(*(s+1)+0));
printf("*(*(s+1)+0) = %dn", *(*(s+1)+1));
//s[i] <==> *(s+i)
printf("s[0] = %p *(s+0) = %pn", s[0], *(s+0));
printf("s[1] = %p *(s+1) = %pn", s[1], *(s+1));
printf("s[2] = %p *(s+2) = %pn", s[2], *(s+2));
//由于二维数组的数组名是一个行指针 操作的空间大于基本类型了
//所以无法定义一个普通的指针保存二维数组的地址用来操作二维数组
int *p = s;
//(s+1) 偏移一行 4个int
//(p+1) 偏移一个int 无法按行操作
printf("*(p+1) = %dn", *(p+1));
**s = 520;
//**p = 1314; 普通的一级指针无法 取**操作
//二维数组的遍历
int i = 0;
int j = 0;
for(i = 0; i < 3; i++){
for(j = 0; j < 4;j++){
//printf("%d ", s[i][j]);
//printf("%d ", *(s[i]+j));
printf("%d ", *(*(s+i)+j));
}
printf("n");
}
return 0;
}
数组指针
本质是一个指针,指向一个二维数组,数组指针也叫行指针。
数组指针多用于 函数中 二维数组传参。
#include <stdio.h>
int main(int argc, const char *argv[])
{
int s[3][4] = {{1,2,3,4},
{5,6,7,8},
{9,10,11,12}};
//定义数组指针的格式
//数据类型 (*指针名)[列数];
int (*p)[4] = s;
//p+1操作的就是一行了
//数组指针指向二维数组后 有如下等价关系
//s[i][j] <==> *(s[i]+j) <==> *(*(s+i)+j)
//p[i][j] <==> *(p[i]+j) <==> *(*(p+i)+j)
//二维数组的遍历
int i = 0;
int j = 0;
for(i = 0; i < 3; i++){
for(j = 0; j < 4; j++){
//printf("%d ", s[i][j]);
//printf("%d ", *(s[i]+j));
//printf("%d ", *(*(s+i)+j));
//printf("%d ", p[i][j]);
//printf("%d ", *(p[i]+j));
printf("%d ", *(*(p+i)+j));
}
printf("n");
}
return 0;
}
指针数组
本质是一个数组,数组中每个元素都是一个指针。
#include <stdio.h>
int main(int argc, const char *argv[])
{
//可以使用二维数组来保存多个字符串
char name1[4][64] = {"zhangsan",
"lisi",
"xiaohong",
"fulajimier.fulajimiluoweiqi.pujing"};
printf("%sn", name1[0]);
printf("%sn", name1[1]);
printf("%sn", name1[2]);
printf("%sn", name1[3]);
//但是这种用法,会造成内存空间的严重浪费,因为二维数组每行的列数必须得一样
//并且得以最长的为准
//可以使用指针数组来处理这种情况
//定义指针数组的格式
//数据类型 *数组名[下标];
//所为的指针数组 本质就是数组 只不过每个元素 都是一个指针
//char *name2[4]; //定义了一个指针数组 数组名为 name
char *name2[4] = {NULL}; //定义了一个指针数组 数组名为 name
//数组中有4个元素 每个元素都是一个 char *指针
//注意要和数组指针区分开:char (*name2)[4] = NULL; 这种写法就是数组指针了
//数组中的一个元素的操作就和 一个 char * 类型的指针操作是一样的
name2[0] = "zhangsan";
name2[1] = "lisi";
name2[2] = "xiaohong";
name2[3] = "fulajimier.fulajimiluoweiqi.pujing";
printf("name2[0] = %sn", name2[0]);
printf("name2[1] = %sn", name2[1]);
printf("name2[2] = %sn", name2[2]);
printf("name2[3] = %sn", name2[3]);
printf("sizeof(name2) = %ldn", sizeof(name2));//==sizeof(char *) * 4 = 32
return 0;
}
指针数组的应用:main函数传参
#include <stdio.h>
int main(int argc, const char *argv[])
{
//argc 是执行程序时 命令行的参数的个数
//包括可执行文件名(a.out)在内
printf("%dn", argc);
//argv 是一个指针数组 数组中的指针分别执行
//执行程序时 命令行传的参数
//如 ./a.out aa bb cc dd
printf("%dn", argc);//5
printf("%sn", argv[0]);// ./a.out
printf("%sn", argv[1]);// aa
printf("%sn", argv[2]);// bb
printf("%sn", argv[3]);// cc
printf("%sn", argv[4]);// dd
return 0;
}
指针和字符串
#include <stdio.h>
int main(int argc, const char *argv[])
{
//可以定义一个字符数组保存字符串
//s1是数组 定义在栈区
//栈区的数据是允许修改的
char s1[12] = "hello world";
s1[0] = 'H';
printf("s1 = %sn", s1);
//栈区定义多个数组 即使存储了相同的字符串 他们也都时在不同空间的
char s2[12] = "hello world";
printf("s1 = %p s2 = %pn", s1, s2);//不一样
//也可以定义个指针 指向字符串
//指针直接指向字符串 指向的字符串常量
//字符串常量是不允许修改的
char *p1 = "hello world";
//p1[0] = 'H';//错误的 常量区的内容不能修改 一修改就会段错误
printf("p1 = %sn", p1);
//不管定义多少个指针 只要指向相同的字符串 那么地址都是一样的
char *p2 = "hello world";
char *p3 = "hello world";
printf("p1 = %p p2 = %p p3 = %pn", p1, p2, p3);//一样的
return 0;
}
二级指针
二级指针是用来保存一级指针的地址的。
一般多用于函数中 一级指针的地址作为参数传递时。
int a = 10; //变量a
int *p = &a; //一级指针 p 保存变量的地址
int **q = &p; //二级指针 q 保存一级指针的地址
上面操作之后,有如下的等价关系:
a <==> *p <==> **q
&a <==> p <==> *q
&p <==> q
变量 一级指针 二级指针 的关系示意图
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a = 10;
int *p = &a;
int **q = &p;
printf("a = %d *p = %d **q = %dn", a, *p, **q);
printf("&a = %p p = %p *q = %pn", &a, p, *q);
printf("&p = %p q = %pn", &p, q);
**q = 520;//通过二级指针 修改变量的值
printf("a = %d *p = %d **q = %dn", a, *p, **q);
//一级指针能保存一级指针的地址
//但是只能保存 一级指针没法做 ** 操作
int *temp = &p;//能保存
printf("temp = %pn", temp);
//**temp = 1314;//但是不能取 **操作
//所以 一般不这样使用
return 0;
}
const关键字
const关键字 如果用来修饰变量,表示通过变量名不能修改变量的值。
const int a = 10;
a = 20;//错误的 const 修饰的变量 值不能修改
const 修饰指针:注意下面的用法
const int *p;
int const *p;
int * const p;
const int * const p;
#include <stdio.h>
int main(int argc, const char *argv[])
{
const int a = 520;
printf("a = %dn", a);//520 读a的值
int b = a;//没问题 读取a的值 赋给b一份儿
//a = 1314;//错误的 不能通过 a 修改他的值
//const int *p;
//int const *p;
//int * const p;
//const int * const p;
//const 修饰指针时 要看 const 和 * 的相对位置
//如果 const 在 * 的左边 表示修饰的是 *p :
// *p 不能改变 不能通过指针 修改指向的内容的值
// p 可以变 指针的指向能修改
//如果 const 在 * 的右边 表示修饰的是 p :
// p 不能改变 指针的指向不能修改
// *p 可以变 可以通过指针 修改指向的内容的值
//const int *p;
int m = 10;
int n = 20;
const int *p1 = &m;
//*p1 = 520;//错误的 不能通过指针 修改指向的内容的值
m = 520;//通过m自己 是可以修改的
p1 = &n;//正确的 指针的指向能修改
//int const *p;
//和上面的用法一样 一般上面的用法比较常用
//int * const p;
int *const p2 = &m;
*p2 = 1314;//正确的 可以通过指针修改指向的内容的值
printf("*p2 = %d m = %dn", *p2, m);
//p2 = &n;//错误的 指针的指向不能修改
//const int * const p;
//指针的指向不能修改
//也不能通过指针 修改指向的内容的值
const int * const p = &m;
//*p = 123;//错误的
//p = &n;//错误的
return 0;
}
函数
概念
将实现某些功能的代码封装成代码块,当想使用这个功能时,只直接通过代码块的名字
就可以调用该部分的代码,无需每次重复写该代码,这个代码块就叫函数,代码块的名字
就叫函数名。如:strcpy printf 我们使用时,直接通过名字就能调到对应的功能。
函数的定义和调用
定义的格式
返回值类型 函数名(形参列表){
函数体,也就是实现功能的代码块
return 函数的返回值;
}
#include <stdio.h>
//全局函数一旦定义好之后 就可以在其他函数中调用了
void my_hello(void){
printf("function my_hellon");
}
//函数的定义一般定义在 main 函数外面 也就是全局处
//C语言的的语法是支持局部定义函数的 但是局部定义的函数 只在局部可用
//我们一般使用的时候 都是将函数定义在全局处 方便其他函数调用
//第一个int 是函数的返回值类型,如果函数没有返回值 可以写成 void
//my_add 是函数名 是一个标识符 要符合标识符的命名规范
//(int x, int y) 是函数的形式参数列表,简称形参表 作用是告诉调用者
// 如果使用该函数需要传递几个什么类型的参数
// 如果函数没有参数 ()里面可以空着 也可以写成 void 但是注意 () 必须要写
//{} 里面就是函数体 也就是 函数实现功能的代码块
int my_add(int x, int y){
printf("%dn", x+y);
my_hello();//没有参数的函数调用 () 必须要写
return 0;//这是函数的返回值 返回的值要和 函数名前面的类型 一致
//如果没有返回值 可以不写 或者 写 return;
printf("hello world");//函数中遇到 return 就立即返回 后面的代码都不执行了
}
int main(int argc, const char *argv[])
{
printf("-----start-----n");
//通过函数名 就可以调用函数了
//调用的时候,() 里面是函数的 实际参数列表 简称 实参表
// 实参的作用是用来 初始化 形参 注意类型和个数要和形参表一致
// 注意 是从左到右依次初始化
my_add(10, 20);//遇到函数调用时 就跳转到对应的代码执行
//执行完函数后 会返回到调用处 继续向下执行
//函数如果不被调用,函数体是不会执行的
printf("-----end-----n");
return 0;
}
函数的声明
#include <stdio.h>
//在一个代码中编写函数时,如果只是简单的将所有的函数都定义在
//main函数的上面,在main函数中是都可以调用的,但是函数之间互相调用的
//时候,就可能出现不认识的情况,为了避免这种情况 就需要用到函数的声明
//函数声明写在代码的最上面 能保证函数调用是不会出现不认识的情况
//函数声明的格式 用 分号 ; 替换 {函数体}
void my_function1();
void my_function2();
int main(int argc, const char *argv[])
{
my_function1();
return 0;
}
void my_function1(){
printf("hellon");
my_function2();
}
void my_function2(){
printf("hqyjn");
}
函数的参数
函数为什么要有参数?
当函数实现功能的过程中,需要执行代码块,代码块需要用到某些值,
但是在函数内部又没有这些值,所以就需要调用函数时,将这些值以参数的形式传过来
#include <stdio.h>
//函数功能:计算两个整数的和
void my_add(int x, int y){
//函数的形参也需要 操作系统分配空间 在栈区
//函数形参的作用主用是用于将实参的值保存一份儿
//在函数内部进行使用
//函数内部如何修改 形参 都不会影响到实参 因为
//形参和实参是在两块不同的内存空间上的
printf("x = %d y = %dn", x, y);//10 20
x = 100;
y = 200;
printf("x = %d y = %dn", x, y);//100 200
int ret = x+y;
printf("ret = %dn", ret);//300
}
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
my_add(a, b);//函数调用的时候 实参的类型和个数
//要和定义函数时形参的类型和个数保持一致
printf("a = %d b = %dn", a, b);//10 20
printf("---end--n");
return 0;
}
函数的返回值
函数为什么要有返回值?
当函数执行完毕后,有些场景需要用到函数执行的结果,
这是就需要将结果返回给函数调用处。
一般情况下,自己封装的函数,都是有返回值的,因为可以通过返回值来判断函数的执行情况。
#include <stdio.h>
#include <stdlib.h>
//函数声明时 可以只写类型 不写形参名
int my_sum(int);
int main(int argc, const char *argv[])
{
int num = atoi(argv[1]);
//ret中保存的就是函数的返回值
int ret = my_sum(num);
//后面的代码中 使用 ret 就相当于 使用了 函数的返回值
int i = 0;
for(i = 0; i < ret; i++){
printf("hello worldn");
}
//如果调用处不关心 函数的返回值 也可以不接他
//不接受返回值 并不会影响 函数中的代码运行
my_sum(100);
return 0;
}
//定义时 必须要写 形参名 因为函数内部要使用这个名字
int my_sum(int n){
int sum = 0;
int i = 0;
for(i = 1; i <= n; i++){
sum += i;
}
return sum;//如果有需要 可以将函数运算的结果 返回给调用处
//注意 return 后面的表达式 的类型 要和 函数名前的类型一致
}
函数的传参方式
全局传参 --了解即可,实际开发过程中基本不使用这种方式
#include <stdio.h>
//没有被任何{}扩住的变量 称之为 全局变量
//全局变量如果不初始化 默认是 0
//全局变量的生命周期是整个程序结束 生命周期:何时被回收
//全局变量的作用域是整个文件 作用域:在哪个范围可以访问他
int num = 100;
//函数功能 值自增
void my_func(){
//这种被 {} 扩住的叫局部变量 生命周期和作用域都是最近的{}
int num2 = 100;
num++;
num2++;
}
int main(int argc, const char *argv[])
{
//如果局部变量和全局变量重名 采用局部优先原则
//int num = 1314;
printf("main:num = %dn", num);//100
num = 520;
printf("main:num = %dn", num);//520
my_func();
printf("main:num = %dn", num);//521
//printf("num2 = %dn", num2);//错误的 已经出了 num2 的作用域了
return 0;
}
值传递(复制传参)
#include <stdio.h>
//形参只是将实参的值保存了一份
//不管函数中如何修改 形参 实参都不会发生变化
//原因是 他们根本就不在同一块内容空间 互不影响
int my_add(int x, int y){
printf("&x = %p &y = %pn", &x, &y);
printf("x = %d y = %dn", x, y);
x = 100;
y = 200;
return x+y;
}
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
printf("&a = %p &b = %pn", &a, &b);
printf("a = %d b = %dn", a, b);
int ret = my_add(a, b);
printf("ret = %dn", ret);
printf("&a = %p &b = %pn", &a, &b);
printf("a = %d b = %dn", a, b);
return 0;
}
地址传递(地址传参)
#include <stdio.h>
//如果想在函数中改变 实参的值
//需要将实参的地址传给函数
//下面的函数中 x 和 y 相当于 值传递 z 相当于地址传递
int my_add(int x, int y, int *z){
printf("&x = %p &y = %p z = %pn", &x, &y, z);
printf("x = %d y = %dn", x, y);
*z = x+y;
}
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
int c = 0;//用来存储计算的结果
printf("&a = %p &b = %p &c = %pn", &a, &b, &c);
printf("a = %d b = %d c = %dn", a, b, c);
int ret = my_add(a, b, &c);
//printf("ret = %dn", ret);//ret一般用来判断函数执行成功与失败
printf("&a = %p &b = %p &c = %pn", &a, &b, &c);
printf("a = %d b = %d c = %dn", a, b, c);
return 0;
}
数组的传参方式
#include <stdio.h>
//打印字符串的函数
//因为字符串有明显的结束标志 '' 所以只需要传首地址就行
void my_printf_char(char *p){
printf("%sn", p);
}
//遍历整形一维数组的函数
//整形数组是没有 '' 来标志结束的 所以需要首地址 和 长度 才能访问 !!!
void my_printf_int1(int *p, int len){// 比较常用的用法
int i = 0;
for(i = 0; i < len; i++){
printf("%d ", p[i]);
}
//p[0] = 1314;//函数中通过指针修改数组成员 实参会发生变化
//因为本身传递的就是数组的首地址
//通过p是直接修改了数组的元素
printf("n");
}
//一维数组传参时 这种用法也可以
//注意这种用法 p的本质也是一个指针 [6] 叫代码自注释
void my_printf_int11(int p[6], int len){// 不常用
printf("sizeof(p) = %ldn", sizeof(p));//8
int i = 0;
for(i = 0; i < len; i++){
printf("%d ", p[i]);
}
printf("n");
}
//遍历二维数组的函数
//需要使用数组指针
void my_printf_int2(int (*p)[4], int hang){
int i = 0;
int j = 0;
for(i = 0; i < hang; i++){
for(j = 0; j < 4; j++){
printf("%d ", p[i][j]);
}
printf("n");
}
}
int main(int argc, const char *argv[])
{
char buff[32] = "hello";
my_printf_char(buff);
int s1[6] = {10,20,30,40,50,60};
my_printf_int1(s1, 6);
my_printf_int11(s1, 6);
int s2[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
my_printf_int2(s2, 3);
return 0;
}
指针函数
本质是一个函数,返回值是一个指针类型。
#include <stdio.h>
//返回值是一个指针类型的函数 就叫做指针函数
char *my_strcpy(char *dest, const char *src){
char *temp = dest;
while(*src != ''){
*dest = *src;
dest++;
src++;
}
*dest = '';
//指针函数可以返回由参数传递过来的地址
return temp;
}
char value2 = 'M';
char *func(){
char value = 'H';
//注意 指针函数不能返回 局部变量的地址
//因为局部变量占用的空间在函数结束时就被系统回收了
//return &value;//错误的
//可以返回全局变量的地址
//或者 static 修饰的局部变量的地址
return &value2;
}
int main(int argc, const char *argv[])
{
char s1[32] = "beijing";
char s2[32] = "hello world";
char *ret = my_strcpy(s1, s2);
printf("s1 = %sn", s1);
printf("s2 = %sn", s2);
printf("ret = %sn", ret);
char *p = func();
printf("%cn", *p);//'M'
return 0;
}
函数指针
本质是一个指针,指向一个函数。
#include <stdio.h>
int my_add(int x, int y){
return x+y;
}
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
printf("%d + %d = %dn", a, b, my_add(a, b));
//定义一个函数指针 指向一个函数
//格式
// 返回值类型 (*函数指针名)(函数的形参表);
int (*p)(int, int) = NULL;
//定义了一个函数指针 名字叫p 可以指向一个
//返回值为int 形参类表为 (int, int) 的函数
//让指针指向函数 函数名就是函数的首地址
p = my_add;
//指向函数后 通过函数指针 也可以调用函数
printf("%d + %d = %dn", a, b, p(a, b));
return 0;
}
函数指针经常使用的场景:回调函数。
#include <stdio.h>
int my_add(int x, int y){
return x+y;
}
int my_sub(int x, int y){
return x-y;
}
//该函数只知道要对两个数做运算 但是具体做什么运算 它不知道
//所以预留了一个函数指针做位形参,具体的运算由
//jisuan函数的调用者传递的第三个参数决定
int jisuan(int x, int y, int (*p)(int, int)){
//也就是说 在函数里面通过函数指针调用函数 具体调用哪个函数
//取决于 jisuan函数的调用者传递的第三个参数
//相当于 你传什么 我就回头去调用什么 所以 叫做回调函数
return p(x,y);
}
int main(int argc, const char *argv[])
{
int a = 10;
int b = 20;
printf("%dn", jisuan(a, b, my_add));//30
printf("%dn", jisuan(a, b, my_sub));//-10
return 0;
}