C语言小惑

1. C语言小惑

1. 操作符

1.1 -按位取反(~)

问题:为什么打印出-1?

解答:

  1. 在32位操作系统里面,整数存放的位数为32位

整数a=0

00000000000000000000000000000000

11111111111111111111111111111111 ~a

  1. 一个整数的二进制的表示有3种,分别为原码、反码、补码。而整数在内存中存储的是补码

    -1:

    原码:1,0000000000000000000000000000001

    (第一位为符号位,1表示负数)

    先将原码转换成反码(符号位不变,其余按位取反):

    反码:1,1111111111111111111111111111110

    补码:1,1111111111111111111111111111111

    1. 计算机打印的是原码,所以补码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

  1. static修饰全局变量,使得这个全局变量只能在自己所在的源文件内部可以使用,其他源文件不能使用!
  2. 全局变量,在其他源文件内部可以被使用,是因为全局变量具有外部链接属性,但是被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将程序生成了为静态库:

  1. 右击“解决问题方案”,选中最下面的“属性”

在“配置属性”中“配置类型”选中“静态库(.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 数组名

数组名是数组首元素的地址,但有两个例外:

  1. sizeof(数组名) - 数组名表示整个数组 - 计算的是整个数组的大小单位是字节
  2. &数组名 - 数组名表示还真能干数组 - 取出的是整个数组的地址
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 指针类型的意义:
  1. 指针类型决定了:指针解引用的权限有多大
int main()
{
	int a = 0x11223344;
    char* pc = &a;
    *pc = 0;
}

a的地址为11 22 33 44,char类型指针解引用使得a的地址变为 00 22 33 44。

  1. 指针类型决定了:指针走一步,能走多远

7.2 野指针

概念:野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)

如何避免野指针:

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放即使置NULL
  4. 指针使用之前检查有效性
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位高地址。红色框内,地址是从低到高,存储的数据地址也是从低到高,所以这个编译器采取的是小端存储模式。

为什么有大小端之分:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值