文章目录
因为最近要准备面试,所以又回来补一下基础整理一下笔记
一、宏定义
1.1、宏定义的生效环节
宏定义,就是一个预处理命令,有预处理器来完成工作,主要工作就是以下四个部分:
- 头文件的文件引入(#incclude);
- 条件编译(#if #elif #endif);
- 宏扩展;
- 行控制;
1.2、条件编译
一般来说,代码的每一行都需要被编译,但是有的时候为我们不需要一些代码,所以我们需需要在这些代码上加入条件,让只满足条件的代码进行编译。
举个常的例子:
这是一段开源库的代码
#ifdef __cplusplus
extern "C" {
#endif
void hello();
#ifdef __cplusplus
}
#endif
为了方便c和c++的去区分,所以在条件上添加的exten"c"这个条件,如果采用gcc进行编译,则不会生效。如果采用g++进行编译,则exten"c"这个条件就会满足,可以用c代码来调用。
包括为我们再平时使用的if else可以叫做条件编译。
1.3、平台已经编译好的宏定义
因为在一些大型系统上,需要这些定义,所以这些定义在系统上已经被广泛使用了
FILE:当前源代码文件名;
LINE:当前源代码的行号;
FUNCTION:当前执行的函数名;
DATE:编译日期;
TIME:编译时间;
#include <stdio.h>
int main()
{
printf("file name: %s, function name = %s, current line:%d \n", __FILE__, __FUNCTION__, __LINE__);
return 0;
}
1.4、宏扩展
所谓的宏扩展就是代码替换,也就是为我们平常说的 #define
主要作用为一下几点
- 减少重复的代码;
- 完成一些通过 C 语法无法实现的功能(字符串拼接);
- 动态定义数据类型,实现类似 C++ 中模板的功能;
- 程序更容易理解、修改(例如:数字、字符串常亮);
1.4.1 宏扩展的好处
- 不需要检查参数,更灵活的传参;
- 直接对宏进行代码扩展,执行时不需要函数调用;
- 如果同一个宏在多处调用,不会增加代码体积;
举个例子
(1)使用宏定义
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
int main()
{
printf("max: %d \n", MAX(1, 2));
}
使用函数
int max(int a, int b)
{
if (a > b)
return a;
return b;
}
int main()
{
printf("max: %d \n", max(1, 2));
}
可能这样看不出什么,但是如果把两个整形数换成浮点数呢?
是不是在函数上就会出血现类型不匹配的错误。但是在宏定义上就不会。
1.4.2 宏扩展的#和##符号
- #号:把参数换成字符串;
- ##号:把两个参数连接起来;
#include <stdio.h>
#define STR(X) #X
int main()
{
printf("output of 123: %s\n",STR(123));
return 0;
}
这里用 %s输出的哈
这样就把参数变成了字符串
#include <stdio.h>
#define name(x,y) x##y
int main()
{
char string[] = (hello world);
printf("%s",name(str,ing));
return 0;
}
1.5、可变参数的处理
1.5.1:可变参数
在代码中,我们使用三个点(…)来表示可变参数。使用 (VA_ARGS )来表示可变参数。
#include <stdio.h>
#define name(...) printf(__VA_ARGS__)
int main()
{
name("this name is :%d\n",1);
return 0;
}
我测试过了,只能打印数字。
1.5.2:自定义参数名
但是如果在三个点之前加入一参数名,那就要使用这个参数名来代替(VA_ARGS )
#include <stdio.h>
#define name(argc...) printf(argc)
int main()
{
name("this name is :%d\n",2);
return 0;
}
1.5.3:可变参数为0的处理
#define name(buf,argc...) printf(buf,argc)
int main()
{
name("this name is :%d\n",3);
return 0;
}
这样编译没啥问题,可以通过。
但是我们修改一下代码
#include <stdio.h>
#define name(buf,...) printf(buf, __VA_ARGS__ )
int main()
{
name("this name \n");
return 0;
}
他会报错,
如何解决,加上两个## 注意,这里的##不是连接而是自动删除
Ok啦。即使是使用自己定义的参数名,也一样,需要在前面加上##。
二、指针和数组之间的区别
2.1、数组和指是如何访问数据的
**数组的访问方式:**
char a[9] = "abcdeefg";
a是一个数组,编译器里面有一个符号表a,他的地址是 4444(这也就是数组的首地址),数组后面所有的字符都可以通过这个地址+偏移量找到,编译器也不需要知道数组的总长度。
指针的访问方式:
char c = 'F';
char *p = &c;
p是一个指针,在编译器符号表里面有一个符号p,他的地址是4444,这个地址(4444)里面存放的内容是(5555),他会把这个内容(5555)作为指向对象向的地址,并从中取得字符F;
2.2、数组的偏移和大小
#include <stdio.h>
int main()
{
char s[20] = "hello world!\n";
int a = sizeof(s[0]);
printf("每个数组的长度是%d\n",a);
printf("这个数组的地址是:%p\n",s);
printf("这个数组的首地址是:%p\n",&s[0]);
printf("这个数组偏移一位的地址是:%p\n",&s[1]);
return 0;
}
很明显。数组是char类型的,占用1个字节,数组的名字就是这个数组的地址,数组的第一位数据处存方的也是也这个地址,这也证明了上面的那个数组访问的方式,他是访问地址,按照数组存放数据类型偏移进行读取。char占用一字节,偏移一位就是地址往后+1。
2.3、指针偏移方式
#include <stdio.h>
int main()
{
char *p = "hello world!\n";
int a = sizeof(p);
printf("每个指针的长度是%d\n",a);
printf("这个指针的地址是:%p\n",p);
printf("这个指针偏移一位的地址是:%p\n",p+1);
return 0;
}
我是64位操作系统,这里这里我也定义的是char类型的指针,但是指针占用8个字节啊,虽然地址偏移量都一样,但是长度不一样。
2.4、指针访问数组
#include <stdio.h>
int main()
{
char a[] = "abcdefg";
char *p = a;
printf("数组的第三个数是:%c\n",a[2]);
printf("数组的第三个数是:%c\n",*(+2));
printf("数组的第三个数地址是:%p\n",&a[2]);
printf("数组的第三个数地址是:%p\n",*p+2);
return 0;
}
三、位和字节的操作
3.1位操作
3.1.1按位或
在二进制中,只要有1,那么运算过后就是1.
符合: |
举个例子:
0100 0010
和
0000 0000
进行或运算,那么运算结果就是
0100 0010
3.1.2 按位与
两个数进行与运算,必须两个都是1才能是1,其余都是0
符号: &
举个例子
0100 0011
和
0100 1101
进行与运算,得到的结果是
0100 0001
3.1.3左移
符号 : <<
原来的二进制
0100 0001
左移一位后的二进制
1000 0010
简单理解来说,左移就是整体往左边移动。
最左边的那个0呢?移走了,先不管他。最右边需需要补上一个0。
3.1.4右移
符号: >>
道理与左移一样
0100 0001 >> 0010 0000
3.1.5 取反
符号: ~
~(0011 0101) = 1100 1010
就0变1,1变0;
3.1.6按位异或
符号:^
(10010011) ^ (00111101) = (10101110)
运算符 ^ 逐位比较两个运算对象。对于每个位,如果两个运算对象中有且只有 1 位 为 1, 结果为 1。其余都为0。
3.2相关用法
这个不知道怎么说,感觉说的不对,但是事实就是这么回事。我们设置偏移位为后第1位和第2位,所以我们说的打开关闭只针对第1位和第2位
这些操作一般用在寄存器(通过0和1来组成位组合)的设置上。
假设所有原来的配为1001 0110
偏移位置都是0000 0011
3.2.1 打开对应位置
在寄存器上我需要打开某一个特定值。采用或操作
1001 0110 |= 0000 0011
结果
1001 0111
这样就打开了到第1位,将它置为1,而其余的不改变
3.2.2 关闭对应位置
1001 0110 &=~ 0000 0011
这里位什么要取反,是因为要保证我们只改变这一个
0000 0011 取反 -> 1111 1100
再和 之前的1001 0110 进行或运算
1001 0110 &= 1111 1100 -> 1001 0100
这样就关闭了第2位将它置为0,如果不取反我们是不是第2位还是1而第3位和第5低8位变为0了呢,(因为后面的值是偏移值,大多都为0,所以要取反,具体的根据实际来,我目前还没有遇到过或运算不取反的)
3.2.3 切换位
就是打开关闭的,关闭打开的
1001 0110 ^= 0000 0011
结果
1001 0101
打开第1位,关闭第2位
3.2.4 提取位
unsigned long color = 0x123456;
unsigned char blue, green, red;
red = color & 0000 0011;
green = (color >> 8) & 0000 0011;
blue = (color >> 16) & 0000 0011;
RGB是按照RGB顺序排列的每个占8位,所以往后偏移就可以了(代码意思就是这样,但是原理我解释不清楚)
3.3如何判断机器字节大小端
#include <stdio.h>
int main(void)
{
int x = 1;
//获取int变量中第 1 个字节地址判断他是否为1
if (*((char *)&x) == 1)
{
printf("little - endian\n");
}
else
{
printf("big - endian\n");
}
return 0;
}
原理是这样的:
在C语言里面只有char是占一个字节的,所以我们只能2通过char来判断首地址的。更加详细的可以百度一下。
4、结构体
4.1、结构体声明
结构体就好像一个人一样,你有名字,结构体也有名字,这是别人怎么认识你的最重要的东西。
你想要什么,你就可以在结构体里面定义什么。
你想要钱,你定义自己的钱,你说你不想当一个男的,太累,你想当一个女的,你直接在结构体定义成一个女的就可以。
举个例子
struct xhh
{
int Age; //年龄
char Sex[24]; //性别
float Money; //有没有钱
};//这个分号不能省略!不能!不能!
假如他是一个外国人,我们记不住他的名字怎么办,我们给他取一个呗
typedef struct //typedef就好像公安局一样,有他佐证才可以这样起名字
{
int Age; //年龄
char Sex[24]; //性别
float Money; //有没有钱
}ATM;//把名字写在后面
4.2如何使用结构体
struct xhh //typedef就好像公安局一样,有他佐证才可以这样起名字
{
int Age; //年龄
char Sex[24]; //性别
float Money; //有没有钱
};//把名字写在后面
int main()
{
struct xhh s={
.Age = 24, //注意是逗号
.Sex = "man",
.Money = 1.35,
};
return 0;
}
这样就对结构体初始化完毕了,当然还有一些方法.
这是最标准的方法。
struct xhh //typedef就好像公安局一样,有他佐证才可以这样起名字
{
int Age; //年龄
char Sex[24]; //性别
float Money; //有没有钱
};//把名字写在后面
int main()
{
struct xhh s={24,"man",13.5};
return 0;
}
其余的也不要学了,因为妈妈说:不好的不要学。
关于链表,因为面试没遇到过,所以我也不知道写啥,还有就是写不动了。写了五个多小时。累了。。
希望对大家有帮助。