本文最后更新于117 天前,其中的信息可能已经过时,如有错误请发送邮件到shikeAB@outlook.com。
【转载】C语言入门到精通,这一篇就够了(13万字笔记)2
函数基本概念
- C源程序是由函数组成的
- 例如: 我们前面学习的课程当中,通过main函数+scanf函数+printf函数+逻辑代码就可以组成一个C语言程序
- C语言不仅提供了极为丰富的库函数, 还允许用户建立自己定义的函数。用户可把自己的算法编写成一个个相对独立的函数,然后再需要的时候调用它
- 例如:你用C语言编写了一个MP3播放器程序,那么它的程序结构如下图所示
- 可以说C程序的全部工作都是由各式各样的函数完成的,所以也把C语言称为函数式语言
函数的分类
- 在C语言中可从不同的角度对函数分类
- 从函数定义的角度看,函数可分为库函数和用户定义函数两种
- 库函数: 由C语言系统提供,用户无须定义,也不必在程序中作类型说明,只需在程序前包含有该函数原型的头文件即可在程序中直接调用。在前面各章的例题中反复用到printf、scanf、getchar、putchar等函数均属此类
- ***用户定义函数:***由用户按需编写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用
- 从函数执行结果的角度来看, 函数可分为有返回值函数和无返回值函数两种
- 有返回值函数: 此类函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值。(必须指定返回值类型和使用return关键字返回对应数据)
- 无返回值函数: 此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值。(返回值类型为void, 不用使用return关键字返回对应数据)
- 从主调函数和被调函数之间数据传送的角度看,又可分为无参函数和有参函数两种
- 无参函数: 在函数定义及函数说明及函数调用中均不带参数。主调函数和被调函数之间不进行参数传送。
- 有参函数: 在函数定义及函数说明时都有参数,称为形式参数(简称为形参)。在函数调用时也必须给出参数,称为实际参数(简称为实参)
函数的定义
- 定义函数的目的
- 将一个常用的功能封装起来,方便以后调用
-
自定义函数的书写格式
返回值类型 函数名(参数类型 形式参数1,参数类型 形式参数2,…) {
函数体;
返回值;
}
- 示例
int main(){
printf("hello world\n");
retrun 0;
}
- 定义函数的步骤
- 函数名:函数叫什么名字
- 函数体:函数是干啥的,里面包含了什么代码
- 返回值类型: 函数执行完毕返回什么和调用者
- 无参无返回值函数定义
- 没有返回值时return可以省略
- 格式:
void 函数名() { 函数体; }
- 示例:
// 1.没有返回值/没有形参 // 如果一个函数不需要返回任何数据给调用者, 那么返回值类型就是void void printRose() { printf(" {@}\n"); printf(" |\n"); printf(" \\|/\n"); // 注意: \是一个特殊的符号(转意字符), 想输出\必须写两个斜线 printf(" |\n"); // 如果函数不需要返回数据给调用者, 那么函数中的return可以不写 }
- 无参有返回值函数定义
- 格式:
返回值类型 函数名() { 函数体; return 值; }
- 示例:
int getMax() { printf("请输入两个整数, 以逗号隔开, 以回车结束\n"); int number1, number2; scanf("%i,%i", &number1, &number2); int max = number1 > number2 ? number1 : number2; return max; }
- 有参无返回值函数定义
- 形式参数表列表的格式:
类型 变量名,类型 变量2,......
- 格式:
void 函数名(参数类型 形式参数1,参数类型 形式参数2,…) { 函数体; }
- 示例:
void printMax(int value1, int value2) { int max = value1 > value2 ? value1 : value2; printf("max = %i\n", max); }
- 形式参数表列表的格式:
- 有参有返回值函数定义
- 格式:
返回值类型 函数名(参数类型 形式参数1,参数类型 形式参数2,…) { 函数体; return 0; }
- 示例:
int printMax(int value1, int value2) { int max = value1 > value2 ? value1 : value2; return max; }
- 函数定义注意
-
- 函数名称不能相同
void test() { } void test() { // 报错 }
函数的参数和返回值
- 形式参数
- 在***定义函数***时,函数名后面小括号()中定义的变量称为形式参数,简称形参
- 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。
- 因此,形参只有在函数内部有效,函数调用结束返回主调函数后则不能再使用该形参变量
int max(int number1, int number2) // 形式参数
{
return number1 > number2 ? number1 : number2;
}
- 实际参数
- 在***调用函数***时, 传入的值称为实际参数,简称实参
- 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参
- 因此应预先用赋值,输入等办法使实参获得确定值
int main() {
int num = 99;
// 88, num, 22+44均能得到一个确定的值, 所以都可以作为实参
max(88, num, 22+44); // 实际参数
return 0;
}
- 形参、实参注意点
- 调用函数时传递的实参个数必须和函数的形参个数必须保持一致
int max(int number1, int number2) { // 形式参数 return number1 > number2 ? number1 : number2; } int main() { // 函数需要2个形参, 但是我们只传递了一个实参, 所以报错 max(88); // 实际参数 return 0; }
- 形参实参类型不一致, 会自动转换为形参类型
void change(double number1, double number2) {// 形式参数
// 输出结果: 10.000000, 20.000000
// 自动将实参转换为double类型后保存
printf("number1 = %f, number2 = %f", number1, number2);
}
int main() {
change(10, 20);
return 0;
}
- 当使用基本数据类型(char、int、float等)作为实参时,实参和形参之间只是值传递,修改形参的值并不影响到实参函数可以没有形参
void change(int number1, int number2) { // 形式参数 number1 = 250; // 不会影响实参 number2 = 222; } int main() { int a = 88; int b = 99; change(a, b); printf("a = %d, b = %d", a, b); // 输出结果: 88, 99 return 0; }
- 返回值类型注意点
- 如果没有写返回值类型,默认是int
max(int number1, int number2) {// 形式参数 return number1 > number2 ? number1 : number2; }
- 函数返回值的类型和return实际返回的值类型应保持一致。如果两者不一致,则以返回值类型为准,自动进行类型转换
int height() {
return 3.14;
}
int main() {
double temp = height();
printf("%lf", temp);// 输出结果: 3.000000
}
- 一个函数内部可以多次使用return语句,但是return语句后面的代码就不再被执行
int max(int number1, int number2) {// 形式参数 return number1 > number2 ? number1 : number2; printf("执行不到"); // 执行不到 return 250; // 执行不到 }
函数的声明
- 在C语言中,函数的定义顺序是有讲究的:
- 默认情况下,只有后面定义的函数才可以调用前面定义过的函数
- 如果想把函数的定义写在main函数后面,而且main函数能正常调用这些函数,那就必须在main函数的前面进行函数的声明, 否则
- 系统搞不清楚有没有这个函数
- 系统搞不清楚这个函数接收几个参数
- 系统搞不清楚这个函数的返回值类型是什么
- 所以函数声明,就是在函数调用之前告诉系统, 该函数叫什么名称, 该函数接收几个参数, 该函数的返回值类型是什么
- 函数的声明格式:
- 将自定义函数时{}之前的内容拷贝到调用之间即可
- 例如:
int max( int a, int b );
- 或者:
int max( int, int );
// 函数声明
void getMax(int v1, int v2);
int main(int argc, const char * argv[]) {
getMax(10, 20); // 调用函数
return 0;
}
// 函数实现
void getMax(int v1, int v2) {
int max = v1 > v2 ? v1 : v2;
printf("max = %i\n", max);
}
- 函数的声明与实现的关系
- 声明仅仅代表着告诉系统一定有这个函数, 和这个函数的参数、返回值是什么
- 实现代表着告诉系统, 这个函数具体的业务逻辑是怎么运作的
- 函数声明注意点:
- 函数的实现不能重复, 而函数的声明可以重复
// 函数声明 void getMax(int v1, int v2); void getMax(int v1, int v2); void getMax(int v1, int v2); // 不会报错 int main(int argc, const char * argv[]) { getMax(10, 20); // 调用函数 return 0; } // 函数实现 void getMax(int v1, int v2) { int max = v1 > v2 ? v1 : v2; printf("max = %i\n", max); }
- 函数声明可以写在函数外面,也可以写在函数里面, 只要在调用之前被声明即可
int main(int argc, const char * argv[]) { void getMax(int v1, int v2); // 函数声明, 不会报错 getMax(10, 20); // 调用函数 return 0; } // 函数实现 void getMax(int v1, int v2) { int max = v1 > v2 ? v1 : v2; printf("max = %i\n", max); }
- 当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数再作声明
// 函数实现
void getMax(int v1, int v2) {
int max = v1 > v2 ? v1 : v2;
printf("max = %i\n", max);
}
int main(int argc, const char * argv[]) {
getMax(10, 20); // 调用函数
return 0;
}
- 如果被调函数的返回值是整型时,可以不对被调函数作说明,而直接调用
int main(int argc, const char * argv[]) { int res = getMin(5, 3); // 不会报错 printf("result = %d\n", res ); return 0; } int getMin(int num1, int num2) {// 返回int, 不用声明 return num1 < num2 ? num1 : num2; }
main函数分析
- main的含义:
- main是函数的名称, 和我们自定义的函数名称一样, 也是一个标识符
- 只不过main这个名称比较特殊, 程序已启动就会自动调用它
- return 0;的含义:
- 告诉系统main函数是否正确的被执行了
- 如果main函数的执行正常, 那么就返回0
- 如果main函数执行不正常, 那么就返回一个非0的数
- 返回值类型:
- 一个函数return后面写的是什么类型, 函数的返回值类型就必须是什么类型, 所以写int
- 形参列表的含义
- int argc :
- 系统在启动程序时调用main函数时传递给argv的值的个数
- const char * argv[] :
- 系统在启动程序时传入的的值, 默认情况下系统只会传入一个值, 这个值就是main函数执行文件的路径
- 也可以通过命令行或项目设置传入其它参数
- int argc :
- 函数练习
- 写一个函数从键盘输入三个整型数字,找出其最大值
- 写一个函数求三个数的平均值
递归函数(了解)
- 什么是递归函数?
- 一个函数在它的函数体内调用它自身称为递归调用
void function(int x){ function(x); }
- 递归函数构成条件
- 自己搞自己
- 存在一个条件能够让递归结束
- 问题的规模能够缩小
- 示例:
- 获取用户输入的数字, 直到用户输入一个正数为止
void getNumber(){
int number = -1;
while (number < 0) {
printf("请输入一个正数\n");
scanf("%d", &number);
}
printf("number = %d\n", number);
}
void getNumber2(){
int number = -1;
printf("请输入一个正数abc\n");
scanf("%d", &number);
if (number < 0) {
// 负数
getNumber2();
}else{
// 正数
printf("number = %d\n", number);
}
}
-
递归和循环区别
- 能用循环实现的功能,用递归都可以实现
- 递归常用于"回溯", “树的遍历”,"图的搜索"等问题
- 但代码理解难度大,内存消耗大(易导致栈溢出), 所以考虑到代码理解难度和内存消耗问题, 在企业开发中一般能用循环都不会使用递归
-
递归练习
- 有5个人坐在一起,问第5个人多少岁?他说比第4个人大两岁。问 第4个人岁数,他说比第3个人大两岁。问第3个人,又说比第2个 人大两岁。问第2个人,说比第1个人大两岁。最后问第1个人, 他说是10岁。请问第5个人多大?
- 用递归法求N的阶乘
- 设计一个函数用来计算B的n次方
进制基本概念
-
什么是进制?
- 进制是一种计数的方式,数值的表示形式
-
常见的进制
- 十进制、二进制、八进制、十六进制
-
进制书写的格式和规律
- 十进制 0、1、2、3、4、5、6、7、8、9 逢十进一
- 二进制 0、1 逢二进一
- 书写形式:需要以0b或者0B开头,例如: 0b101
- 八进制 0、1、2、3、4、5、6、7 逢八进一
- 书写形式:在前面加个0,例如: 061
- 十六进制 0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F 逢十六进一
- 书写形式:在前面加个0x或者0X,例如: 0x45
-
练习
- 1.用不同进制表示如下有多少个方格
- 2.判断下列数字是否合理
00011 0x001 0x7h4 10.98 0986 .089-109 +178 0b325 0b0010 0xffdc 96f 96.0f 96.oF -.003
进制转换
- 10 进制转 2 进制
- 除2取余, 余数倒序; 得到的序列就是二进制表示形式
- 例如: 将十进制(97) 10转换为二进制数
- 2 进制转 10 进制
- 每一位二进制进制位的值 * 2的当前索引次幂; 再将所有位求出的值相加
- 例如: 将二进制01100100转换为十进制
01100100 索引从右至左, 从零开始 第0位: 0 * 2^0 = 0; 第1位: 0 * 2^1 = 0; 第2位: 1 * 2^2 = 4; 第3位: 0 * 2^3 = 0; 第4位: 0 * 2^4 = 0; 第5位: 1 * 2^5 = 32; 第6位: 1 * 2^6 = 64; 第7位: 0 * 2^7 = 0; 最终结果为: 0 + 0 + 4 + 0 + 0 + 32 + 64 + 0 = 100
- 2 进制转 8 进制
- 三个二进制位代表一个八进制位, 因为3个二进制位的最大值是7,而八进制是逢8进1
- 例如: 将二进制01100100转换为八进制数
从右至左每3位划分为8进制的1位, 不够前面补0 001 100 100 第0位: 100 等于十进制 4 第1位: 100 等于十进制 4 第2位: 001 等于十进制 1 最终结果: 144就是转换为8进制的值
- 2 进制转 16 进制
- 四个二进制位代表一个十六进制位,因为4个二进制位的最大值是15,而十六进制是逢16进1
- 例如: 将二进制01100100转换为十六进制数
从右至左每4位划分为16进制的1位, 不够前面补0 0110 0100 第0位: 0100 等于十进制 4 第1位: 0110 等于十进制 6 最终结果: 64就是转换为16进制的值
- 其它进制转换为十进制
- 系数 * 基数 ^ 索引 之和
十进制 --> 十进制 12345 = 10000 + 2000 + 300 + 40 + 5 = (1 * 10 ^ 4) + (2 * 10 ^ 3) + (3 * 10 ^ 2) + (4 * 10 ^ 1) + (5 * 10 ^ 0) = (1 * 10000) + (2 + 1000) + (3 * 100) + (4 * 10) + (5 * 1) = 10000 + 2000 + 300 + 40 + 5 = 12345 规律: 其它进制转换为十进制的结果 = 系数 * 基数 ^ 索引 之和 系数: 每一位的值就是一个系数 基数: 从x进制转换到十进制, 那么x就是基数 索引: 从最低位以0开始, 递增的数
二进制 --> 十进制 543210 101101 = (1 * 2 ^ 5) + (0 * 2 ^ 4) + (1 * 2 ^ 3) + (1 * 2 ^ 2) + (0 * 2 ^ 1) + (1 * 2 ^ 0) = 32 + 0 + 8 + 4 + 0 + 1 = 45 八进制 --> 十进制 016 = (0 * 8 ^ 2) + (1 * 8 ^ 1) + (6 * 8 ^ 0) = 0 + 8 + 6 = 14 十六进制 --> 十进制 0x11f = (1 * 16 ^ 2) + (1 * 16 ^ 1) + (15 * 16 ^ 0) = 256 + 16 + 15 = 287
- 十进制快速转换为其它进制
- 十进制除以
基数
取余, 倒叙读取
十进制 --> 二进制 100 --> 1100100 100 / 2 = 50 0 50 / 2 = 25 0 25 / 2 = 12 1 12 / 2 = 6 0 6 / 2 = 3 0 3 / 2 = 1 1 1 / 2 = 0 1 十进制 --> 八进制 100 --> 144 100 / 8 = 12 4 12 / 8 = 1 4 1 / 8 = 0 1 十进制 --> 十六进制 100 --> 64 100 / 16 = 6 4 6 / 16 = 0 6
- 十进制除以
十进制小数转换为二进制小数
- 整数部分,直接转换为二进制即可
- 小数部分,使用"乘2取整,顺序排列"
- 用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,直到积中的小数部分为零,或者达到所要求的精度为止
- 然后把取出的整数部分按顺序排列起来, 即是小数部分二进制
- 最后将整数部分的二进制和小数部分的二进制合并起来, 即是一个二进制小数
- 例如: 将12.125转换为二进制
// 整数部分(除2取余)
12
/ 2
------
6 // 余0
/ 2
------
3 // 余0
/ 2
------
1 // 余1
/ 2
------
0 // 余1
//12 --> 1100
// 小数部分(乘2取整数积)
0.125
* 2
------
0.25 //0
0.25
* 2
------
0.5 //0
0.5
* 2
------
1.0 //1
0.0
// 0.125 --> 0.001
// 12.8125 --> 1100.001
二进制小数转换为十进制小数
- 整数部分按照二进制转十进制即可
- 小数部分从最高位开始乘以2的负n次方, n从1开始
- 例如: 将 1100.001转换为十进制
// 整数部分(乘以2的n次方, n从0开始)
0 * 2^0 = 0
0 * 2^1 = 0
1 * 2^2 = 4
1 * 2^3 = 8
// 1100 == 8 + 4 + 0 + 0 == 12
// 小数部分(乘以2的负n次方, n从0开始)
0 * (1/2) = 0
0 * (1/4) = 0
1 * (1/8) = 0.125
// .100 == 0 + 0 + 0.125 == 0.125
// 1100.001 --> 12.125
- 练习:
- 将0.8125转换为二进制
- 将0.1101转换为十进制
0.8125
* 2
--------
1.625 // 1
0.625
* 2
--------
1.25 // 1
0.25
* 2
--------
0.5 // 0
* 2
--------
1.0 // 1
0.0
// 0. 8125 --> 0.1101
1*(1/2) = 0.5
1*(1/4)=0.25
0*(1/8)=0
1*(1/16)=0.0625
//0.1101 --> 0.5 + 0.25 + 0 + 0.0625 == 0.8125
原码反码补码
- 计算机只能识别0和1, 所以计算机中存储的数据都是以0和1的形式存储的
- 数据在计算机内部是以补码的形式储存的, 所有数据的运算都是以补码进行的
- 正数的原码、反码和补码
- 正数的原码、反码和补码都是它的二进制
- 例如: 12的原码、反码和补码分别为
0000 0000 0000 0000 0000 0000 0000 1100
0000 0000 0000 0000 0000 0000 0000 1100
0000 0000 0000 0000 0000 0000 0000 1100
- 负数的原码、反码和补码
- 二进制的最高位我们称之为符号位, 最高位是0代表是一个正数, 最高位是1代表是一个负数
- 一个负数的原码, 是将该负数的二进制最高位变为1
- 一个负数的反码, 是将该数的原码
除了符号位
以外的其它位取反 - 一个负数的补码, 就是它的反码 + 1
- 例如: -12的原码、反码和补码分别为
0000 0000 0000 0000 0000 0000 0000 1100 // 12二进制 1000 0000 0000 0000 0000 0000 0000 1100 // -12原码 1111 1111 1111 1111 1111 1111 1111 0011 // -12反码 1111 1111 1111 1111 1111 1111 1111 0100 // -12补码
- 负数的原码、反码和补码逆向转换
- 反码 = 补码-1
- 原码= 反码最高位不变, 其它位取反
1111 1111 1111 1111 1111 1111 1111 0100 // -12补码 1111 1111 1111 1111 1111 1111 1111 0011 // -12反码 1000 0000 0000 0000 0000 0000 0000 1100 // -12原码
- 为什么要引入反码和补码
- 在学习本节内容之前,大家必须明白一个东西, 就是计算机只能做加法运算, 不能做减法和乘除法, 所以的减法和乘除法内部都是用加法来实现的
- 例如: 1 - 1, 内部其实就是 1 + (-1);
- 例如: 3 * 3, 内部其实就是 3 + 3 + 3;
- 例如: 9 / 3, 内部其实就是 9 + (-3) + (-3) + (-3);
- 首先我们先来观察一下,如果只有原码会存储什么问题
- 很明显, 通过我们的观察, 如果只有原码, 1-1的结果不对
// 1 + 1 0000 0000 0000 0000 0000 0000 0000 0001 // 1原码 +0000 0000 0000 0000 0000 0000 0000 0001 // 1原码 --------------------------------------- 0000 0000 0000 0000 0000 0000 0000 0010 == 2 // 1 - 1; 1 + (-1); 0000 0000 0000 0000 0000 0000 0000 0001 // 1原码 +1000 0000 0000 0000 0000 0000 0000 0001 // -1原码 --------------------------------------- 1000 0000 0000 0000 0000 0000 0000 0010 == -2
- 在学习本节内容之前,大家必须明白一个东西, 就是计算机只能做加法运算, 不能做减法和乘除法, 所以的减法和乘除法内部都是用加法来实现的
- 正是因为对于减法来说,如果使用原码结果是不正确的, 所以才引入了反码
- 通过反码计算减法的结果, 得到的也是一个反码;
- 将计算的结果符号位不变其余位取反,就得到了计算结果的原码
- 通过对原码的转换, 很明显我们计算的结果是-0, 符合我们的预期
// 1 - 1; 1 + (-1); 0000 0000 0000 0000 0000 0000 0000 0001 // 1反码 1111 1111 1111 1111 1111 1111 1111 1110 // -1反码 --------------------------------------- 1111 1111 1111 1111 1111 1111 1111 1111 // 计算结果反码 1000 0000 0000 0000 0000 0000 0000 0000 // 计算结果原码 == -0
- 虽然反码能够满足我们的需求, 但是对于0来说, 前面的负号没有任何意义, 所以才引入了补码
- 由于int只能存储4个字节, 也就是32位数据, 而计算的结果又33位, 所以最高位溢出了,符号位变成了0, 所以最终得到的结果是0
// 1 - 1; 1 + (-1); 0000 0000 0000 0000 0000 0000 0000 0001 // 1补码 1111 1111 1111 1111 1111 1111 1111 1111 // -1补码 --------------------------------------- 10000 0000 0000 0000 0000 0000 0000 0000 // 计算结果补码 0000 0000 0000 0000 0000 0000 0000 0000 // == 0
位运算符
- 程序中的所有数据在计算机内存中都是以二进制的形式储存的。
- 位运算就是直接对整数在内存中的二进制位进行操作
- C语言提供了6个位操作运算符, 这些运算符只能用于整型操作数
符号 | 名称 | 运算结果 |
---|---|---|
& | 按位与 | 同1为1 |
| | 按位或 | 有1为1 |
^ | 按位异或 | 不同为1 |
~ | 按位取反 | 0变1,1变0 |
<< | 按位左移 | 乘以2的n次方 |
>> | 按位右移 | 除以2的n次方 |
- 按位与:
- 只有对应的两个二进位均为1时,结果位才为1,否则为0
- 规律: 二进制中,与1相&就保持原位,与0相&就为0
9&5 = 1
1001
&0101
------
0001
- 按位或:
- 只要对应的二个二进位有一个为1时,结果位就为1,否则为0
9|5 = 13
1001
|0101
------
1101
- 按位异或
- 当对应的二进位相异(不相同)时,结果为1,否则为0
- 规律:
- 相同整数相的结果是0。比如55=0
- 多个整数相^的结果跟顺序无关。例如: 567=576
- 同一个数异或另外一个数两次, 结果还是那个数。例如: 577 = 5
9^5 = 12
1001
^0101
------
1100
- 按位取反
- 各二进位进行取反(0变1,1变0)
~9 =-10
0000 0000 0000 0000 0000 1001 // 取反前
1111 1111 1111 1111 1111 0110 // 取反后
// 根据负数补码得出结果
1111 1111 1111 1111 1111 0110 // 补码
1111 1111 1111 1111 1111 0101 // 反码
1000 0000 0000 0000 0000 1010 // 源码 == -10
- 位运算应用场景:
- 判断奇偶(按位或)
偶数: 的二进制是以0结尾 8 -> 1000 10 -> 1010 奇数: 的二进制是以1结尾 9 -> 1001 11 -> 1011 任何数和1进行&操作,得到这个数的最低位 1000 &0001 ----- 0000 // 结果为0, 代表是偶数 1011 &0001 ----- 0001 // 结果为1, 代表是奇数
- 权限系统
enum Unix { S_IRUSR = 256,// 100000000 用户可读 S_IWUSR = 128,// 10000000 用户可写 S_IXUSR = 64,// 1000000 用户可执行 S_IRGRP = 32,// 100000 组可读 S_IWGRP = 16,// 10000 组可写 S_IXGRP = 8,// 1000 组可执行 S_IROTH = 4,// 100 其它可读 S_IWOTH = 2,// 10 其它可写 S_IXOTH = 1 // 1 其它可执行 }; // 假设设置用户权限为可读可写 printf("%d\n", S_IRUSR | S_IWUSR); // 384 // 110000000
- 交换两个数的值(按位异或)
a = a^b; b = b^a; a = a^b;
- 按位左移
- 把整数a的各二进位全部左移n位,高位丢弃,低位补0
- 由于左移是丢弃最高位,0补最低位,所以符号位也会被丢弃,左移出来的结果值可能会改变正负性
- 规律: 左移n位其实就是乘以2的n次方
- 把整数a的各二进位全部左移n位,高位丢弃,低位补0
2<<1; //相当于 2 *= 2 // 4
0010
<<0100
2<<2; //相当于 2 *= 2^2; // 8
0010
<<1000
- 按位右移
- 把整数a的各二进位全部右移n位,保持符号位不变
- 为正数时, 符号位为0,最高位补0
- 为负数时,符号位为1,最高位是补0或是补1(取决于编译系统的规定)
- 规律: 快速计算一个数除以2的n次方
- 把整数a的各二进位全部右移n位,保持符号位不变
2>>1; //相当于 2 /= 2 // 1
0010
>>0001
4>>2; //相当于 4 /= 2^2 // 1
0100
>>0001
- 练习:
- 写一个函数把一个10进制数按照二进制格式输出
#include
void printBinary(int num);
int main(int argc, const char * argv[]) {
printBinary(13);
}
void printBinary(int num){
int len = sizeof(int)*8;
int temp;
for (int i=0; i>(31-i); //每次移动的位数
int t = temp&1; //取出最后一位
if(i!=0&&i%4==0)printf(" "); printf("%d",t);
}
}
变量内存分析
- 内存模型
- 内存模型是线性的(有序的)
- 对于 32 机而言,最大的内存地址是2^32次方bit(4294967296)(4GB)
- 对于 64 机而言,最大的内存地址是2^64次方bit(18446744073709552000)(171亿GB)
- CPU 读写内存
- CPU 在运作时要明确三件事
- 存储单元的地址(地址信息)
- 器件的选择,读 or 写 (控制信息)
- 读写的数据 (数据信息)
- CPU 在运作时要明确三件事
- 如何明确这三件事情
- 通过地址总线找到存储单元的地址
- 通过控制总线发送内存读写指令
- 通过数据总线传输需要读写的数据
- 地址总线: 地址总线宽度决定了CPU可以访问的物理地址空间(寻址能力)
- 例如: 地址总线的宽度是1位, 那么表示可以访问 0 和 1的内存
- 例如: 地址总线的位数是2位, 那么表示可以访问 00、01、10、11的内存
- 数据总线: 数据总线的位数决定CPU单次通信能交换的信息数量
- 例如: 数据总线:的宽度是1位, 那么一次可以传输1位二进制数据
- 例如: 地址总线的位数是2位,那么一次可以传输2位二进制数据
- 控制总线: 用来传送各种控制信号
- 写入流程
- CPU 通过地址线将找到地址为 FFFFFFFB 的内存
- CPU 通过控制线发出内存写入命令,选中存储器芯片,并通知它,要其写入数据。
- CPU 通过数据线将数据 8 送入内存 FFFFFFFB 单元中
-
读取流程
- CPU 通过地址线将找到地址为 FFFFFFFB 的内存
- CPU 通过控制线发出内存读取命令,选中存储器芯片,并通知它,将要从中读取数据
- 存储器将 FFFFFFFB 号单元中的数据 8 通过数据线送入 CPU寄存器中
-
变量的存储原则
- 先分配字节地址大内存,然后分配字节地址小的内存(内存寻址是由大到小)
- 变量的首地址,是变量所占存储空间字节地址(最小的那个地址 )
- 低位保存在低地址字节上,高位保存在高地址字节上
10的二进制: 0b00000000 00000000 00000000 00001010 高字节← →低字节
char类型内存存储细节
- char类型基本概念
- char是C语言中比较灵活的一种数据类型,称为“字符型”
- char类型变量占1个字节存储空间,共8位
- 除单个字符以外, C语言的的转义字符也可以利用char类型存储
字符 | 意义 |
---|---|
\b | 退格(BS)当前位置向后回退一个字符 |
\r | 回车(CR),将当前位置移至本行开头 |
\n | 换行(LF),将当前位置移至下一行开头 |
\t | 水平制表(HT),跳到下一个 TAB 位置 |
\0 | 用于表示字符串的结束标记 |
\ |
代表一个反斜线字符 \ |
\" | 代表一个双引号字符" |
\’ | 代表一个单引号字符’ |
- char型数据存储原理
- 计算机只能识别0和1, 所以char类型存储数据并不是存储一个字符, 而是将字符转换为0和1之后再存储
- 正是因为存储字符类型时需要将字符转换为0和1, 所以为了统一, 老美就定义了一个叫做ASCII表的东东
- ASCII表中定义了每一个字符对应的整数
char ch1 = 'a';
printf("%i\n", ch1); // 97
char ch2 = 97;
printf("%c\n", ch2); // a
- char类型注意点
- char类型占一个字节, 一个中文字符占3字节(unicode表),所有char不可以存储中文
char c = '我'; // 错误写法
- 除转义字符以外, 不支持多个字符
char ch = 'ab'; // 错误写法
- char类型存储字符时会先查找对应的ASCII码值, 存储的是ASCII值, 所以字符6和数字6存储的内容不同
char ch1 = '6'; // 存储的是ASCII码 64 char ch2 = 6; // 存储的是数字 6
- 练习
- 定义一个函数, 实现输入一个小写字母,要求转换成大写输出
类型说明符
- 类型说明符基本概念
- C语言提供了说明长度和说明符号位的两种类型说明符, 这两种类型说明符一共有4个:
- short 短整型 (说明长度)
- long 长整型 (说明长度)
- signed 有符号型 (说明符号位)
- unsigned 无符号型 (说明符号位)
- C语言提供了说明长度和说明符号位的两种类型说明符, 这两种类型说明符一共有4个:
- 这些说明符一般都是用来修饰int类型的,所以在使用时可以省略int
- 这些说明符都属于C语言关键字
short和long
- short和long可以提供不同长度的整型数,也就是可以改变整型数的取值范围。
- 在64bit编译器环境下,int占用4个字节(32bit),取值范围是-2^31 ~ 2^31-1;
- short占用2个字节(16bit),取值范围是-2^15 ~ 2^15-1;
- long占用8个字节(64bit),取值范围是-2^63 ~ 2^63-1
- 总结一下:在64位编译器环境下:
- short占2个字节(16位)
- int占4个字节(32位)
- long占8个字节(64位)。
- 因此,如果使用的整数不是很大的话,可以使用short代替int,这样的话,更节省内存开销。
- 世界上的编译器林林总总,不同编译器环境下,int、short、long的取值范围和占用的长度又是不一样的。比如在16bit编译器环境下,long只占用4个字节。不过幸运的是,ANSI \ ISO制定了以下规则:
- short跟int至少为16位(2字节)
- long至少为32位(4字节)
- short的长度不能大于int,int的长度不能大于long
- char一定为为8位(1字节),毕竟char是我们编程能用的最小数据类型
- 可以连续使用2个long,也就是long long。一般来说,long long的范围是不小于long的,比如在32bit编译器环境下,long long占用8个字节,long占用4个字节。不过在64bit编译器环境下,long long跟long是一样的,都占用8个字节。
#include
int main()
{
// char占1个字节, char的取值范围 -2^7~2^7
char num = 129;
printf("size = %i\n", sizeof(num)); // 1
printf("num = %i\n", num); // -127
// short int 占2个字节, short int的取值范围 -2^15~2^15-1
short int num1 = 32769;// -32767
printf("size = %i\n", sizeof(num1)); // 2
printf("num1 = %hi\n", num1);
// int占4个字节, int的取值范围 -2^31~2^31-1
int num2 = 12345678901;
printf("size = %i\n", sizeof(num2)); // 4
printf("num2 = %i\n", num2);
// long在32位占4个字节, 在64位占8个字节
long int num3 = 12345678901;
printf("size = %i\n", sizeof(num3)); // 4或8
printf("num3 = %ld\n", num3);
// long在32位占8个字节, 在64位占8个字节 -2^63~2^63-1
long long int num4 = 12345678901;
printf("size = %i\n", sizeof(num4)); // 8
printf("num4 = %lld\n", num4);
// 由于short/long/long long一般都是用于修饰int, 所以int可以省略
short num5 = 123;
printf("num5 = %lld\n", num5);
long num6 = 123;
printf("num6 = %lld\n", num6);
long long num7 = 123;
printf("num7 = %lld\n", num7);
return 0;
}
signed和unsigned
- 首先要明确的:signed int等价于signed,unsigned int等价于unsigned
- signed和unsigned的区别就是它们的最高位是否要当做符号位,并不会像short和long那样改变数据的长度,即所占的字节数。
- signed:表示有符号,也就是说最高位要当做符号位。但是int的最高位本来就是符号位,因此signed和int是一样的,signed等价于signed int,也等价于int。signed的取值范围是-2^31 ~ 2^31 - 1
- unsigned:表示无符号,也就是说最高位并不当做符号位,所以不包括负数。
- 因此unsigned的取值范围是:0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111,也就是0 ~ 2^32 - 1
#include
int main()
{
// 1.默认情况下所有类型都是由符号的
int num1 = 9;
int num2 = -9;
int num3 = 0;
printf("num1 = %i\n", num1);
printf("num2 = %i\n", num2);
printf("num3 = %i\n", num3);
// 2.signed用于明确说明, 当前保存的数据可以是有符号的, 一般情况下很少使用
signed int num4 = 9;
signed int num5 = -9;
signed int num6 = 0;
printf("num4 = %i\n", num4);
printf("num5 = %i\n", num5);
printf("num6 = %i\n", num6);
// signed也可以省略数据类型, 但是不推荐这样编写
signed num7 = 9;
printf("num7 = %i\n", num7);
// 3.unsigned用于明确说明, 当前不能保存有符号的值, 只能保存0和正数
// 应用场景: 保存银行存款,学生分数等不能是负数的情况
unsigned int num8 = -9;
unsigned int num9 = 0;
unsigned int num10 = 9;
// 注意: 不看怎么存只看怎么取
printf("num8 = %u\n", num8);
printf("num9 = %u\n", num9);
printf("num10 = %u\n", num10);
return 0;
}
- 注意点:
- 修饰符号的说明符可以和修饰长度的说明符混合使用
- 相同类型的说明符不能混合使用
signed short int num1 = 666;
signed unsigned int num2 = 666; // 报错
数组的基本概念
-
数组,从字面上看,就是一组数据的意思,没错,数组就是用来存储一组数据的
- 在C语言中,数组属于构造数据类型
-
数组的几个名词
- 数组:一组
相同数据类型
数据的有序
的集合 - 数组元素: 构成数组的每一个数据。
- 数组的下标: 数组元素位置的索引(从0开始)
- 数组:一组
-
数组的应用场景
- 一个int类型的变量能保存一个人的年龄,如果想保存整个班的年龄呢?
- 第一种方法是定义很多个int类型的变量来存储
- 第二种方法是只需要定义一个int类型的数组来存储
- 一个int类型的变量能保存一个人的年龄,如果想保存整个班的年龄呢?
#include
int main(int argc, const char * argv[]) {
/*
// 需求: 保存2个人的分数
int score1 = 99;
int score2 = 60;
// 需求: 保存全班同学的分数(130人)
int score3 = 78;
int score4 = 68;
...
int score130 = 88;
*/
// 数组: 如果需要保存`一组``相同类型`的数据, 就可以定义一个数组来保存
// 只要定义好一个数组, 数组内部会给每一块小的存储空间一个编号, 这个编号我们称之为 索引, 索引从0开始
// 1.定义一个可以保存3个int类型的数组
int scores[3];
// 2.通过数组的下标往数组中存放数据
scores[0] = 998;
scores[1] = 123;
scores[2] = 567;
// 3.通过数组的下标从数组中取出存放的数据
printf("%i\n", scores[0]);
printf("%i\n", scores[1]);
printf("%i\n", scores[2]);
return 0;
}
定义数组
- 元素类型 数组名[元素个数];
// int 元素类型
// ages 数组名称
// [10] 元素个数
int ages[10];
初始化数组
- 定义的同时初始化
- 指定元素个数,完全初始化
- 其中在{ }中的各数据值即为各元素的初值,各值之间用逗号间隔
int ages[3] = {4, 6, 9};
- 不指定元素个数,完全初始化
- 根据大括号中的元素的个数来确定数组的元素个数
int nums[] = {1,2,3,5,6};
- 指定元素个数,部分初始化
- 没有显式初始化的元素,那么系统会自动将其初始化为0
int nums[10] = {1,2};
- 指定元素个数,部分初始化
int nums[5] = {[4] = 3,[1] = 2};
- 不指定元素个数,部分初始化
int nums[] = {[4] = 3};
- 先定义后初始化
int nums[3];
nums[0] = 1;
nums[1] = 2;
nums[2] = 3;
- 没有初始化会怎样?
- 如果定义数组后,没有初始化,数组中是有值的,是随机的垃圾数,所以如果想要正确使用数组应该要进行初始化。
int nums[5];
printf("%d\n", nums[0]);
printf("%d\n", nums[1]);
printf("%d\n", nums[2]);
printf("%d\n", nums[3]);
printf("%d\n", nums[4]);
输出结果:
0
0
1606416312
0
1606416414
- 注意点:
- 使用数组时不能超出数组的索引范围使用, 索引从0开始, 到元素个数-1结束
- 使用数组时不要随意使用未初始化的元素, 有可能是一个随机值
- 对于数组来说, 只能在定义的同时初始化多个值, 不能先定义再初始化多个值
int ages[3];
ages = {4, 6, 9}; // 报错
数组的使用
- 通过下标(索引)访问:
// 找到下标为0的元素, 赋值为10
ages[0]=10;
// 取出下标为2的元素保存的值
int a = ages[2];
printf("a = %d", a);
数组的遍历
- 数组的遍历:遍历的意思就是有序地查看数组的每一个元素
int ages[4] = {19, 22, 33, 13};
for (int i = 0; i < 4; i++) {
printf("ages[%d] = %d\n", i, ages[i]);
}
数组长度计算方法
- 因为数组在内存中占用的字节数取决于其存储的数据类型和数据的个数
- 数组所占用存储空间 = 一个元素所占用存储空间 * 元素个数(数组长度)
- 所以计算数组长度可以使用如下方法
数组的长度 = 数组占用的总字节数 / 数组元素占用的字节数
int ages[4] = {19, 22, 33, 13};
int length = sizeof(ages)/sizeof(int);
printf("length = %d", length);
输出结果: 4
练习
- 正序输出(遍历)数组
int ages[4] = {19, 22, 33, 13};
for (int i = 0; i < 4; i++) {
printf("ages[%d] = %d\n", i, ages[i]);
}
- 逆序输出(遍历)数组
int ages[4] = {19, 22, 33, 13};
for (int i = 3; i >=0; i--) {
printf("ages[%d] = %d\n", i, ages[i]);
}
- 从键盘输入数组长度,构建一个数组,然后再通过for循环从键 盘接收数字给数组初始化。并使用for循环输出查看
数组内部存储细节
-
存储方式:
- 1)内存寻址从大到小, 从高地址开辟一块连续没有被使用的内存给数组
- 2)从分配的连续存储空间中, 地址小的位置开始给每个元素分配空间
- 3)从每个元素分配的存储空间中, 地址最大的位置开始存储数据
- 4)用数组名指向整个存储空间最小的地址
-
示例
#include
int main()
{
int num = 9;
char cs[] = {'l','n','j'};
printf("cs = %p\n", &cs); // cs = 0060FEA9
printf("cs[0] = %p\n", &cs[0]); // cs[0] = 0060FEA9
printf("cs[1] = %p\n", &cs[1]); // cs[1] = 0060FEAA
printf("cs[2] = %p\n", &cs[2]); // cs[2] = 0060FEAB
int nums[] = {2, 6};
printf("nums = %p\n", &nums); // nums = 0060FEA0
printf("nums[0] = %p\n", &nums[0]);// nums[0] = 0060FEA0
printf("nums[1] = %p\n", &nums[1]);// nums[1] = 0060FEA4
return 0;
}
- 注意:字符在内存中是以对应ASCII码值的二进制形式存储的,而非上述的形式。
数组的越界问题
- 数组越界导致的问题
- 约错对象
- 程序崩溃
char cs1[2] = {1, 2};
char cs2[3] = {3, 4, 5};
cs2[3] = 88; // 注意:这句访问到了不属于cs1的内存
printf("cs1[0] = %d\n", cs1[0] );
输出结果: 88
为什么上述会输出88, 自己按照"数组内部存储细节"画图脑补
数组注意事项
- 在定义数组的时候[]里面只能写整型常量或者是返回整型常量的表达式
int ages4['A'] = {19, 22, 33};
printf("ages4[0] = %d\n", ages4[0]);
int ages5[5 + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);
int ages5['A' + 5] = {19, 22, 33};
printf("ages5[0] = %d\n", ages5[0]);
- 错误写法
// 没有指定元素个数,错误
int a[];
// []中不能放变量
int number = 10;
int ages[number]; // 老版本的C语言规范不支持
printf("%d\n", ages[4]);
int number = 10;
int ages2[number] = {19, 22, 33} // 直接报错
// 只能在定义数组的时候进行一次性(全部赋值)的初始化
int ages3[5];
ages10 = {19, 22, 33};
// 一个长度为n的数组,最大下标为n-1, 下标范围:0~n-1
int ages4[4] = {19, 22, 33}
ages4[8]; // 数组角标越界
- 练习
- 从键盘录入当天出售BTC的价格并计算出售的BTC的总价和平均价(比如说一天出售了10个比特币)
数组和函数
- 数组可以作为函数的参数使用,数组用作函数参数有两种形式:
- 一种是把数组元素作为实参使用
- 一种是把数组名作为函数的形参和实参使用
数组元素作为函数参数
- 数组的元素作为函数实参,与同类型的简单变量作为实参一样,如果是基本数据类型, 那么形参的改变不影响实参
void change(int val)// int val = number
{
val = 55;
}
int main(int argc, const char * argv[])
{
int ages[3] = {1, 5, 8};
printf("ages[0] = %d", ages[0]);// 1
change(ages[0]);
printf("ages[0] = %d", ages[0]);// 1
}
- 用数组元素作函数参数不要求形参也必须是数组元素
数组名作为函数参数
- 在C语言中,数组名除作为变量的标识符之外,数组名还代表了该数组在内存中的起始地址,因此,当数组名作函数参数时,实参与形参之间不是"值传递",而是"地址传递"
- 实参数组名将该数组的起始地址传递给形参数组,两个数组共享一段内存单元, 系统不再为形参数组分配存储单元
- 既然两个数组共享一段内存单元, 所以形参数组修改时,实参数组也同时被修改了
void change2(int array[3])// int array = 0ffd1
{
array[0] = 88;
}
int main(int argc, const char * argv[])
{
int ages[3] = {1, 5, 8};
printf("ages[0] = %d", ages[0]);// 1
change(ages);
printf("ages[0] = %d", ages[0]);// 88
}
数组名作函数参数的注意点
- 在函数形参表中,允许不给出形参数组的长度
void change(int array[])
{
array[0] = 88;
}
- 形参数组和实参数组的类型必须一致,否则将引起错误。
void prtArray(double array[3]) // 错误写法
{
for (int i = 0; i < 3; i++) {
printf("array[%d], %f", i, array[i]);
}
}
int main(int argc, const char * argv[])
{
int ages[3] = {1, 5, 8};
prtArray(ages[0]);
}
- 当数组名作为函数参数时, 因为自动转换为了指针类型,所以在函数中无法动态计算除数组的元素个数
void printArray(int array[])
{
printf("printArray size = %lu\n", sizeof(array)); // 8
int length = sizeof(array)/ sizeof(int); // 2
printf("length = %d", length);
}
- 练习:
- 设计一个函数int arrayMax(int a[], int count)找出数组元素的最大值
- 从键盘输入3个0-9的数字,然后输出0~9中哪些数字没有出现过
- 要求从键盘输入6个0~9的数字,排序后输出
来自陕西