文章目录
1. C语言小惑
1. 操作符
1.1 -按位取反(~)
问题:为什么打印出-1?
解答:
- 在32位操作系统里面,整数存放的位数为32位
整数a=0
00000000000000000000000000000000
11111111111111111111111111111111 ~a
-
一个整数的二进制的表示有3种,分别为原码、反码、补码。而整数在内存中存储的是补码。
-1:
原码:1,0000000000000000000000000000001
(第一位为符号位,1表示负数)
先将原码转换成反码(符号位不变,其余按位取反):
反码:1,1111111111111111111111111111110
补码:1,1111111111111111111111111111111
-
计算机打印的是原码,所以补码1,1111111111111111111111111111111
打印出来为-1.
-
1.2 左移右移
左移右移都是针对数值的二进制进行移动的。
**左移:**左边丢弃,右边补0
右移:
- 算术右移:右边丢弃,左位补原符号位
- 逻辑右移:右边丢弃,左边补0
在VS2019中,采用的是算术右移
图中,a本身数值没有变化。
注意:对于移位运算符,不要移动负数位,这个是标准未定义的。
1.3 异或运算符(^)
一道面试题:
不能创建临时变量(第三个变量),实现两个数的交换
#include <stdio.h>
int main()
{
int a = 3;
int b = 5;
//解法1
//当a,b过大时,a+b容易发生溢出
a = a + b;
b = a - b;
a = a- b;
//解法2
//a ^ a = 0
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
1.4 sizeof
问题:为什么sizeof(s = a + 2)结果为2。然后打印s的结果为5?
解答:比如源代码test.c文件到可执行文件test.exe的过程中,有编译->链接->运行,sizeof的执行在编译阶段(1.实际上没有计算,只知道a+2要赋值给s,然后就计算了s的大小。2.关于整型提升,sizeof中表达式是不参与计算的,所以就没有整型提升。),而打印s则在运行阶段,此时编译阶段已经结束,就没有计算sizeof中的表达式。
2. static
- static修饰全局变量,使得这个全局变量只能在自己所在的源文件内部可以使用,其他源文件不能使用!
- 全局变量,在其他源文件内部可以被使用,是因为全局变量具有外部链接属性,但是被static修饰之后,就变成了内部链接属性,其他源文件就不能链接到这个静态的全局变量。
3. 分模块编写程序
我们建议将函数的声明放在头文件中,函数的定义放在源文件中,假设编写加法函数,则需要建立add.h头文件和add.c源文件
add.h文件中存放函数的声明:
int Add(int x,int y);
add.c文件存放函数的定义:
int Add(int x,int y)
{
return x+y;
}
然后在主程序中包含add.h文件,然后使用即可:
#include <stdio.h>
#include "add.h"
int main()
{
printf("%d",Add(3,2));
return 0;
}
4. 静态库(.lib)生成与使用
加入程序员A要卖给程序员B减法函数Sub(),但是又不想暴露源码给B,于是A将程序生成了为静态库:
-
右击“解决问题方案”,选中最下面的“属性”
在“配置属性”中“配置类型”选中“静态库(.lib)",然后点应用并确定。
ctrl+F5生成一下。在程序的上一层目录可发现“Debug”文件夹,静态库就在此文件夹中。
然后程序员B在程序中包含Sub.h文件,并在主程序中导入静态库即可:
5. 数组
5.1 数组传参
void fun(int *arr)
{
//error,len == 1
int len = sizeof(arr)/sizeof(arr[0]);
}
int main()
{
int arr[] = {1,2,3,4,5};
fun(arr);
}
**问题:**函数fun中len得出的结果为1,为什么为1呢?我们所想的结果应该为5。
**解答:**当数组作为参数传递给函数时,实际上只是传递了指针,指向数组的首元素。由于int类型的指针大小为4,所以在函数fun中,sizeof(arr)=4,而sizeof(arr[0])元素的大小也为4,所以4/4=1;
正确的写法应该在函数外面计算好数组的长度,然后将数组和数组长度一起作为形参传递给函数
void fun(int *arr,int len)
{
....
}
int main()
{
int arr[] = {1,2,3,4,5};
int len = sizeof(arr)/sizeof(arr[0]);
fun(arr,len);
}
5.2 数组名
数组名是数组首元素的地址,但有两个例外:
- sizeof(数组名) - 数组名表示整个数组 - 计算的是整个数组的大小单位是字节
- &数组名 - 数组名表示还真能干数组 - 取出的是整个数组的地址
int main()
{
int arr[10] = {0};
printf("%p\n",&arr); //1.&arr取出的是数组的地址
printf("%p\n",&arr+1);
printf("%p\n",arr);
printf("%p\n",arr+1);
return 0;
}
由此可见,&arr+1加的是整个数组的大小,而arr+1加的是一个元素的大小。
6. 整型提升
//无符号整型提升,高位补0
7. 指针
7.1 指针类型的意义:
- 指针类型决定了:指针解引用的权限有多大
int main()
{
int a = 0x11223344;
char* pc = &a;
*pc = 0;
}
a的地址为11 22 33 44,char类型指针解引用使得a的地址变为 00 22 33 44。
-
指针类型决定了:指针走一步,能走多远
7.2 野指针
概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
如何避免野指针:
- 指针初始化
- 小心指针越界
- 指针指向空间释放即使置NULL
- 指针使用之前检查有效性
7.3 指针关系运算的标准规定
7.4 函数指针
指向函数的指针。
int Add(int a,int b)
{
return a+b;
}
int main()
{
//函数指针 - 存放函数地址的指针
//&函数名 - 取到的就是函数的地址
//pf就是一个函数指针变量
int (*pf)(int,int) = &Add;
//使用函数指针,对pf解引用找到函数,然后传参
int ret = (*pf)(3,5);
int (*pf)(int,int) = Add;//Add == pf
//ret 与 ret1 等价,*号没什么意义
int ret = (*pf)(3,5);
int ret1 = pf(3,5);
//这两个printf等价
printf("%p\n",&Add);
printf("%p\n",Add);
}
void (*signal(int,void(*)(int)))(int);
//1.signal 和()先结合,说明signal是函数名
//2.signal函数的第一个参数的类型是int,第二个参数的类型是函数指针
//该函数指针,指向一个参数为int,返回类型是void的函数
//3.signal函数的返回类型也是一个函数指针
//该函数指针,指向一个参数为int,返回类型为void的函数
//signal是一个函数的声明
//typedef - 对类型进行重定义
typedef void(*pfun_t)(int);//对void(*)(int)的函数指针类型重命名为pfun_t
pfun_t signal(int,pfun_t);//与第一行语句等价
**函数指针数组:**是一个数组,元素为函数指针
int (*pf1)(int,int) = Add;
int (*pf2)(int,int) = Sub;
int (*pfArr[2])(int,int) = {Add,Sub};//pfArr函数指针数组
8. 结构体
8.1 结构体变量的定义和初始化
8.2 结构体传参
struct stu
{
int age;
char name[20];
int number;
};
void print1(struct stu s)
{
...
}
void print2(struct stu *s)
{
...
}
int main()
{
struct stu s = {...};
print1(s);
print2(&s);
}
问题:上面的print1和print2函数哪个好些?
**解答:**首选print2函数,原因:
函数传参的时候,参数是需要压栈的,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,所以会导致性能的下降。
结论:结构体传参的时候,要传结构体的地址。
9.函数
9.1 函数传参压栈
调用Add函数时,参数压栈。当Add函数调用完成,图中蓝色区域和紫色区域的内存就会被释放掉。
10.内存
1.栈区
代码结果为无限循环hehe。为什么??
如果i的地址和arr的地址恰好差2个字节(由编译器决定,vs中,i和数组arr之间差两个字节),则会出现死循环的情况
如果先定义arr,再定义i,则不会出现死循环,程序会直接报错,越界访问内存。
那为什么在死循环里,则没有进行数组越界访问的报错?原因是程序一直在死循环,没时间报错。
当i和arr地址相差不是2个字节,则会出现内存访问越界。
为什么死循环中,不会出现数组越界访问的报错?因为程序一直在死循环,没时间报错。
11. 数据的存储
数据类型介绍
类型的基本归类
整型家族:
浮点数家族:
float
double
构造类型:
int arr[10]; //类型为int [10]
int arr2[5]; //类型为int [5]
指针类型:
空类型:
整型在内存中的存储
大端小端介绍
蓝色框内,44为低地址,11位高地址。红色框内,地址是从低到高,存储的数据地址也是从低到高,所以这个编译器采取的是小端存储模式。
为什么有大小端之分: