C语言学习
五.操作符
5.单目操作符(2)
sizeof不能用于计算动态分配的内存
在对数组使用sizeof时,返回的是整个数组的大小(所有元素的总字节数)。而对指针使用sizeof时,返回的是指针本身的大小(通常是机器字长的大小)
6.关系操作符
> >= < <= != ==
7.逻辑操作符
&&逻辑与 ||逻辑或
i = a++ && ++b && ++c,其中如果a为0,则后面的代码都不会执行
i = a++ || ++b || ++c,其中如果a为1,则后面的代码都不会执行
8.三目操作符
a > b ? a : b, 若a>b则取a,若不大于,则取b
9.逗号表达式
逗号表达式,就是用逗号隔开的多个表达式,逗号表达式,从左向右依次执行,整个表达式的结果是最后一个表达式的结果
10.下标引用、函数调用、结构成员
下标引用:arr[x]
函数调用:getMax(a, b)
结构成员:obj.name或者obj->name
11.表达式求值
表达式求值的顺序一部分是由操作符的优先级和结合性决定
同样,有些表达式的操作数在求值的过程中可能需要转换为其它类型
隐式类型转换:c的整型算数运算总是至少以缺省整型类型的精度来进行的
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这叫整型提升
整型提升的意义:避免数据丢失或溢出,同时可以保证运算结果的准确性
char类型赋值为整型时,会默认是ASCII码并转换
代码实例:
第二种隐式类型转换:算术转换,如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行
12.操作符的属性
复杂表达式的求值有三个影响的因素:
- 操作符的优先级
- 操作符的结合性
- 是否控制求值顺序
i = i-- - --i是一个有问题的表达式,不同的编译器会计算出不同的结果
六.初始指针
1.指针
指针是编程语言中的对象,利用地址,它的值指向存储在电脑中的某处内存中的值
获取指针:int* p = &a;
指针就是变量,用来存放地址的变量
一个小的内存单元是一个字节
2,指针类型
指针类型决定了指针进行解引用操作的时候,能够访问空间的大小
int* p; *p能访问4个字节
char* p; *p能访问1个字节
double* p; *p能访问8个字节
指针类型还决定了指针的步长
int* p; p+1向后走4个字节
char* p; p+1向后走1个字节
double* p; p+1向后走8个字节
3.指针与数组
指针可以直接修改数组中的数据:
通常,数组名就是首元素地址,也可能是整个数组
4.野指针
野指针就是指针指向的位置是不可知的
原因:
- 指针未初始化,默认是随机值
- 指针越界访问,指针超过数组的长度时,它会变为随机值
- 指针指向的内存空间被释放
注意:全局指针指向局部变量或主程序中的指针指向函数中的变量时会报警告
指针最好初始化,可以int* p = NULL;
5.指针运算
①指针±整数
②指针-指针:不同类型的指针最好不要相减
③指针的关系运算:标准规定:允许指向数组元素的指针与指向数组最后一个元素的后一位的地址比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较,所以,不是所有编译器允许这种运算
6.二级指针
int a = 10;
int* pa = &a;
int** ppa = &pa
其中ppa就是二级指针,**ppa=10;
7.指针数组
定义指针数组:int* arr[3] = {&a, &b, &c};
输出指针数组指向的数据:
七.实用调试技巧
1.调试的步骤
- 发现程序错误的存在
- 以隔离、消除等方式对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
system(“pause”);语句可以让代码在该语句中终止
2.debug和release
debug通常为调试版本,包含一些调试信息,可被调试,文件较大
release通常为发布版本,代码会被自动优化
注意:C语言中并没有内建的越界检查,因此开发者需要小心确保不要越界访问数组
assert头文件中的assert函数(断言),当条件不符合时,直接报错
代码实例:
对于常量,可以用改地址的对应的值来改值,所以一般对常量的地址也用const修饰
区分:const int* p(*p是常量)和int* const p(p是常量)
八.结构体
1.结构体的声明
struct tag
{
member-list;
}全局的结构体变量;
结构体是一些值的集合,这些值可以是不同类型
2.结构体的初始化
①按顺序初始化:struct Person person1 = {“Alice”, 25};
②指定成员初始化:struct Person person2 = {.age = 30, .name = “Bob”};
③嵌套结构体的初始化:struct Student student1 = {“David”, {5, 8, 1999}};
④动态分配并初始化:
struct Person *person3 = malloc(sizeof(struct Person));
if (person3 != NULL) {
person3->age = 22;
strcpy(person3->name, "Charlie");
}
3.结构体传参
两种结构体传参方式:把结构体整个传过去,把结构体的地址传过去
把结构体整个传过去,压栈("压栈"通常指的是将数据存储到栈(stack)数据结构中的操作)消耗的内存过大
结论:建议把结构体的地址传过去
九.数据的存储
1.数据类型
整型家族:
char:unsigned char、signed char(ASCII码值,所以归类为整型)
short:unsigned short、signed short
int:unsigned int、signed int
long:unsigned long、signed long
浮点型家族:
float
double
long double
构造类型:
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
指针类型:
int*
char*
float*
void*
空类型:void(无类型)
2.整型在内存中的存储
计算机中的有符号数有三种表示方法,即原码、反码、补码
三种表示方法均有符号位和数值位,符号位为0则是正数
有符号数的二进制序列:原码
原码取反(除外符号位):反码
反码+1:补码
正数的原、反、补码都相同
对于整型,数据存放在内存中的是补码
大端存储模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
小端存储模式:是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
为什么有大端小端:寄存器宽度大于一个字节,那么必然存在着多个字节安排的问题,所以有大端小端的存储模式
判断当前机器的字节序的小程序:
注解:指针类型决定了指针解引用操作符能访问几个字节:char*访问1个字节,int*访问4个字节
printf格式控制符
%d:用于输出整型
%c:用于输出单个字符
%s:用于输出字符串
%f:用于输出浮点数
%u:用于输出无符号整数
%x:用于输出十六进制数
%o:用于输出八进制数
%p:用于输出指针地址
3.浮点数在内存中的存储
IEEE754规定:任意二进制浮点数V可表示为:-1s*M*2E
其中:
- -1^s表示符号位,当s=0,V为正数
- M表示有效数字
- 2^E表示指数位
IEEE754规定:对于32位的浮点数(单精度),最高的1位是位s,接着的8位是指数E,剩下的23位是数字位
对于64位的浮点数(双精度),最高的1位是位s,接着的11位是E,剩下的52位是数字位
对于有效数字M,M可以写成1.xxx的形式,其中xxxx表示小数部分
IEEE754规定,在计算机内部保存时,默认这个数的第一位总是1,因此可以省去
对于指数E,E是一个无符号整数,但是科学计数法中的E可能出现负数,所以IEEE754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127,对于11位的E,这个中间数是1023,比如2^10的E是10,所以保存为137,即10001001
例子:
float 5.5 -> 101.1 ->-10*1.011*22 -> S=0, M=1.011, E=2 -> 01000000101100000000000000000000 -> 0x40b00000
E取出来的三种情况:
- E不全为0且不全为1:先减127(或1023)得到真实值,再将有效数字M前加上第一位的1
- E全为0:E等于1-127即为真实值,M不再加上第一位的1,因为它本身就很接近0
- E全为1:如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
十.指针详解
1.字符指针
字符指针:char*
一般使用:
char ch = 'w';
char* pc = &ch;
*pc = 'a';
非常规使用:
char *str = "Hello, world!";
在这种情况下,编译器会自动为字符串分配内存,并将其存储在程序的静态存储区域。因此,str 指向的是字符串的第一个字符的地址。
请注意,如果您尝试修改 str 指向的字符串,可能会导致未定义的行为。这是因为在大多数情况下,字符串存储在只读内存区域,因此尝试修改字符串的内容可能会导致程序崩溃或产生其他不可预测的结果。如果您需要修改字符串,应该使用字符数组而不是字符指针来存储它,建议用const修饰该str
2.指针数组
指针组成的数组叫指针数组
一般用法:
int arr1[] = {1, 2, 3};
int arr2[] = {2, 3, 4};
int arr3[] = {3, 4, 5};
int* parr[] = {arr1, arr2, arr3}
3.数组指针
数组指针是一个指针
定义一个数组指针:int(*p)[10] = arr;其中(*p)表示p是指针
数组指针的使用(一般数组指针用在二维以上数组):
int main(){
int arr[3][5]={ {1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7} };
print2DArray(arr, 3, 5);
return 0;
}
void print2DArray(int (*arr)[5], int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
指针数组和数组指针的比较
int arr[5]; arr是一个5个元素的整形数组
int *parr1[5]; arr1是一个数组,数组有10个元素,每个元素的类型是int*,parr1是指针数组
int (*parr2)[10]; parr2是一个指针,该指针指向了一个数组,数组有10个元素,每个元素的类型是int,parr2是数组指针
int (*parr3[10])[5]; parr3是一个数组,该数组有10个元素,每个元素是一个数组指针,该数组指针指向的数组有5个元素,每个元素的类型是int
数组传参
以上方法均ok
上述方法中下四个方法中只有数组指针的那个是OK的
一级指针传参,传的可以是数组名取地址、一级指针
二级指针传参,传的可以是二级指针、一级指针取地址
4.函数指针
函数指针是指向函数的指针
定义一个函数指针:int (*pa)(int, int) = Add;
int是函数的返回类型,pa是指针名,(int, int)是函数的传参列表,Add是函数名
&函数名和函数名本身都是函数的地址