参考链接:
https://www.cnblogs.com/huangdengtao/p/11450539.html
C语言深度剖析 - 博客园找找看
https://www.cnblogs.com/xkfz007/archive/2012/02/27/2369562.html
第 1 章 关键字
数据类型与字节大小
-
所有指针占用内存是相同的
- sizeof(int *) = sizeof(char *) = sizeof(void *)
32 位和 64 位的区别有
char *
、long
、unsigned long。32位系统:
数据类型 | 字节大小 |
---|---|
short | 2 |
int |
4 |
int * |
4 ,32位/8=4B |
unsigned int | 4 |
char |
1 |
char * | 4 |
float | 4 |
double | 8 |
long |
4 |
unsigned long | 4 |
long long | 8 |
- 64位系统:
数据类型 | 字节大小 |
---|---|
short | 2 |
int |
4 |
int * |
8 ,64位/8=8B |
unsigned int | 4 |
char |
1 |
char * | 8 |
float | 4 |
double | 8 |
long | 8 |
unsigned long | 8 |
long long | 8 |
- signed int 范围:-2^31 ~ 2^31-1
- unsigned int 范围:0 ~ 2^32-1
- char 范围:-2^7 ~ 2^7-1
- signed char 范围:-128 ~ 127
- unsigned char 范围:0 ~ 255
- unsigned int == unsigned,int 可省略
- signed int == int,signed 可省略
printf() 函数常用格式控制字符
格式字符 | 说明 |
---|---|
d | 以十进制形式输出带符号整数(整数前不输出符号 +) |
o | 以八进制形式输出无符号整数(前缀 0 不输出) |
x 或 X | 以十六进制形式输出无符号整数(前缀 0x 不输出) |
u | 以十进制形式输出无符号整数 |
c | 以字符形式输出单个字符 |
s | 以字符串形式输出字符串 |
f | 以小数形式输出单、双精度实数,系统默认输出六位小数 |
e 或 E | 以标准指数形式输出单、双精度实数 |
g 或 G | 以 %f 或 %e 格式中输出宽度较短的一种格式输出单、双精度实数 |
p | 以十六进制整数方式输出指针的值(前缀 0x 不输出),位数不够时,左边补 0 |
printf() 函数常用附加格式修饰符
含义 | 格式修饰符 | 说明 |
---|---|---|
标志 | - | 输出数据左对齐,右边填补空格,系统默认为右对齐输出 |
标志 | + | 输出带符号数的正数时前面显示正号(+) |
标志 | 空格 | 输出带符号数的正数时前面显示空格代替正号 |
标志 | 0 | 输出数据时指定左边不使用的空位自动填 0 |
标志 | # | 在八进制和十六进制数前分别显示前导符 0 和 0x |
最小宽度 | m(代表一个整数) | 按宽度 m 输出,若数据长度小于 m,左边补空格,否则按实际输出 |
精度 | .n(代表一个整数) | 对于实数,指定小数点后的位数为 n 位(四舍五入);对于字符串,指定截取字符串前n个字符 |
长度 | l 或 L | 在 d、o、x、u 格式字符前,指定输出精度为 long 型;在 f、e、g 格式字符前,指定输出精度为 double 型 |
长度 | h 或 H | 在 d、o、x、u 格式字符前,指定输出精度为 short 型 |
32 位系统下:
int i = 0;
类型 | 字节大小 (Byte) |
---|---|
sizeof(int) | 4 |
sizeof(i) | 4 |
sizeof int | 错误,2个关键字 不能叠加 |
sizeof i | 4 |
32 位系统下:
int *p = NULL;
int * 指针 32/8=4B
类型 | 字节大小 (Byte) |
---|---|
sizeof(p) | 4,int * 大小 |
sizeof(*p) | 4,int 大小 |
32 位系统下:
int a[100];
类型 | 字节大小 (Byte) |
---|---|
sizeof(a) | 400 |
sizeof(a[100]) | 4,不存在 a[100],但是只讨论类型大小 |
sizeof(&a) | 4,取数组 a 的首地址
|
sizeof(&a[0]) | 4,取数组 a[0] 的首地址
|
- &a[0] 和 &a 值是一样的。
int b[100];
void fun(int b[100])
{
sizeof(b); //4B,取 b 的首地址
}
比较的时候,一般把常量放到左边。
if(NULL != p)
if(0 == x)
break 和 continue 的区别
while(1)
{
if('#' == GetInputChar())
break;或continue;
}
- break:终止本层循环,只有一层循环,循环结束;
- continue:终止本轮循环,进入下一轮循环。
多重循环中,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
void *:任意类型的指针都可以操作。
const 修饰的只读变量
(readonly)
- 修饰一般变量
int const i = 2;
const int i = 2;
等价于 const int i = 2;
- 修饰指针
const int *p;
int const *p;
int * const p;
const int * const p;
等价于
- const
int*p; -
intconst *p; -
int* const p; - const
int* const p;
可以看出
1 和 2 是相同的,const 修饰 p,p 是指针,p 是指针指向的对象,不可变;
3 const 修饰 p,p 不可变,p 指向的对象;
4 前一个 const 修饰 *p,后一个 const 修饰 p,指针 p 和 p 指向的对象都不可变。
extern 在另一个文件的声明
a.c 中定义 int i = 10;
b.c 中声明 extern int i; //正确
b.c 中声明 extern int i = 10; //错误,重定义
struct 和 class 的区别
struct:public 属性
class:private 属性
union 联合体
- 大小足够容纳最宽的成员;
- 大小能被其包含的所有基本数据类型的大小所整除;
- 所有的数据成员具有相同的起始地址;
- 适用于一些数据不可能在同一时间同时被用到。
大小端模式
int i = 1;
- 大端模式(ARM 平台):字数据的
高字节
存储在低地址
中,低字节
则存储在高地址
中。
高存低,低存高。
0x0 | 0x0 | 0x0 | 0x1 |
---|---|---|---|
低地址 | → | → | 高地址 |
- 小端模式(x86 平台):字数据的
高字节
存储在高地址
中,低字节
则存储在低地址
中。
低存低,高存高。
0x0 | 0x0 | 0x0 | 0x1 |
---|---|---|---|
高地址 | ← | ← | 低地址 |
经典案例:
在 x86 系统下:
#include "stdio.h"
void main(void)
{
int a[5] = {1,2,3,4,5};
int *ptr1 = (int *)(&a+1);
int *ptr2 = (int *)(a+1);
printf("%x, %x", ptr1[-1], *ptr2);
}
运行结果:5, 2000000
&a+1 = 数组首地址 + sizeof(a) = &a + 5* sizeof(int),1 的大小取决于 a 的类型
。
(int)a+1 = &(a[0][1 2 3] + a[1][0]),此时 1 根据 a 的类型取 1 字节。
typedef 的使用
typedef struct student
{
//code
}Stu_st, *Stu_pst;
- struct student stu1 和 Stu_st stu1 没有区别;
- struct student *stu2、Stu_pst stu2 和 Stu_st * stu2 没有区别。
第 2 章 符号
接续符 \
反斜杠的下一行之前不能有空格。
逻辑运算符
|| 和 && 的短路现象,只要前面的表达式能决定结果,后面的表达式不再执行。
++、-- 操作符
#include "stdio.h"
void main()
{
int i = 3;
int j = 0;
j = (++i)+(++i)+(++i);
printf("(++i)+(++i)+(++i) = %d", j);
}
- 运行结果:(++i)+(++i)+(++i) = 16
- 分析:++ 优先级高于 +,前2个先做运算,再与第3个作运算。
#include "stdio.h"
void main()
{
int i = 3;
int j = 0;
j = (i++)+(i++)+(i++);
printf("(i++)+(i++)+(i++) = %d", j);
}
- 运行结果:(i++)+(i++)+(i++) = 12
- 分析:++ 优先级高于 +,前2个先做运算,再与第3个作运算。
优先级表
优先级 | 运算符 | 名称 | 结合方向 |
---|---|---|---|
1 | [ ],( ),.,-> | 数组下标,圆括号,成员选择(对象),成员选择(指针) | → |
2 | -,(类型),++ ,-- ,*,&,!,~,sizeof |
负号,强制类型转换,自增 ,自减 ,取值,取地址,逻辑非,按位取反,长度运算 |
← |
3 | /,*,% | 除,乘,余数(取模) | → |
4 |
+ ,-
|
加 ,减
|
→ |
5 | <<,>> | 左移,右移 | → |
6 | >,>=,<,<= | 大于,大于等于,小于,小于等于 | → |
7 | ==,!= | 等于,不等于 | → |
8 | & | 按位与 | → |
9 | ^ | 按位异或 | → |
10 | | | 按位或 | → |
11 | && | 逻辑与 | → |
12 | || | 逻辑或 | → |
13 | ?: | 条件运算符 | ← |
14 | =,/=,*=,%=,+=,-=,<<=,>>=,&=,^=,|= | 赋值,除后,乘后,取余后,加后,减后,左移后,右移后,按位与后,按位异或后,按位或后 | ← |
15 | , | 逗号运算符 | → |
容易出错的优先级问题
优先级 | 表达式 | 错误结果 | 实际结果 |
---|---|---|---|
. 高于 * | *p.f | p所指对象的字段f(*p).f | 对p取f偏移,作为指针*(p.f) |
[ ] 高于 * | int *ap[ ] | ap是指向int数组的指针int (*ap)[ ] | ap是元素为int指针的数组int *(ap[ ]) |
函数 ( ) 高于 * | int *fp() | fp是个函数指针,所指函数返回int int(*fp)() | fp是个函数,返回int* int *(fp()) |
== 和 != 高于位运算 | (val&mask != 0) | (val&mask) != 0 | val & (mask != 0) |
== 和 != 高于赋值符 | c=getchar()!= EOF | (c=getchar()) != EOF | c=(getchar() != EOF) |
算术运算符高于位移 | msb << 4+lsb | (msb<<4) + lsb | msb << (4+lsb) |
逗号优先级最低 | i=1, 2 | i=(1, 2) | (i=1), 2 |
第 3 章 预处理
内存对齐(1、2、4、8最大偶数对齐)
- 为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问。
- 未对齐的情况
一个字或者双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,需要两次总线周期
来访问内存。 - 对齐的情况
一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期
中被访问。
- 未对齐的情况
字节对齐规则
- 结构体等复杂类型按照它最长的成员的对齐方式进行对齐;
- 对齐后的长度必须是成员中最大的对齐参数的整数倍;
- 数组按照
类型
进行对齐,而不是按它的长度
; - 对齐的边界一定是1、2、4、8、16、32、64...中的一个。
#include "stdio.h"
void main()
{
struct TestStruct1
{
char c1; //1
short s; //2
char c2; //1
int i; //4
};
struct TestStruct1 a;
printf("%d\n", sizeof(a));
printf("c1 %p, s %p, c2 %p, i %p\n",
(unsigned int)(void *)&a.c1 - (unsigned int)(void *)&a,
(unsigned int)(void *)&a.s - (unsigned int)(void *)&a,
(unsigned int)(void *)&a.c2 - (unsigned int)(void *)&a,
(unsigned int)(void *)&a.i - (unsigned int)(void *)&a);
}
运行结果:
12
c1 0000000000000000, s 0000000000000002, c2 0000000000000004, i 0000000000000008分析:通过地址可以很容易看出内存分配情况
1 | 1 | 1 | char、short | |
---|---|---|---|---|
1 | char | |||
1 | 1 | 1 | 1 | int,按最大的对齐 |
#include "stdio.h"
void main()
{
struct TestStruct1
{
char c1; //1
char c2; //1
short s; //2
int i; //4
};
struct TestStruct1 a;
printf("%d", sizeof(a));
}
- 运行结果:8
- 分析
1 | 1 | 1 | 1 | char、char、short |
---|---|---|---|---|
1 | 1 | 1 | 1 | int,按最大的对齐 |
#include "stdio.h"
void main()
{
struct Test
{
int Num; //4
char *pcName; //4
short sDate; //2
char cha[2]; //1*2=2,最大长度是 1,不是 2
short sBa[4]; //2*4=8,最大长度是 2,不是 8
}*p;
printf("%d", sizeof(*p));
}
- 运行结果:20
- 分析:如果结构体中有数组,需要考虑数组个数,但是内存还是按
类型
来算的,不是类型*个数
。
第 4 章 指针和数组
&a[0] 和 &a 的区别
对于结果而言,&a[0] == &a
- &a[0]:数组首元素的首地址;
- &a:数组的首地址。
指针变量在 32 位系统下,永远占 4 字节。
内存与尺子
#include <stdio.h>
int main()
{
int a[3][2]={(0,1),(2,3),(4,5)};
int *p;
p=a [0];
printf("%d",p[0]);
}
- 运行结果:1
- 分析:,运算符先计算左边,在计算右边。右边操作数作为整个表达式的结果。
a[0][0] | a[0][1] | a[1][0] | a[1][1] | a[2][0] | a[2][1] |
---|
int a [3][2]={ 1, 3, 5};
数组作为函数参数
- 当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其元素首地址的指针。
#include "stdio.h"
void fun(char a[ ]) //a[5] a[10] *a 都可以
{
int i = sizeof(a); //传递的是地址
char c = a[3];
printf("%d %c\n", i, c);
}
void main()
{
char b[10] = "abcdefg";
fun(b); //类型为 char *
}
- 运行结果:4 d
- 分析:传递的是参数,不检验具体的标号。
第 5 章 内存管理
内存释放
定义指针变量的同时最好初始化为 NULL,用完指针之后也将指针变量的值设置为 NULL。
free 完之后,一定要给指针置 NULL。
栈、堆和静态区
内存分为三个部分:堆、栈和静态区。
- 堆:由 malloc 系列函数或 new 操作符分配的内存。其生命周期由 free 或 delete 决定。在没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但容易出错;
- 栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容也会自动被销毁。其特点是效率高,但空间大小有限;
- 静态区:保存自动全局变量和 static 变量(包括 static 全局和局部变量)。静态区的内容在总个程序的生命周期内都存在,由编译器在编译的时候分配。
asset 宏可以帮助我们定位错误,而不是排除错误。
malloc 分配内存
char *p1 = “abcdefg”;
char *p2 = (char *)malloc(sizeof(char)*strlen(p1)+1*sizeof(char));
strcpy(p2,p1);
- 分析:需要考虑字符串常量的结束标志“\0”,不要因为 char 类型大小为 1 个 byte 就省略 sizof(char)这种写法。这样只会使你的代码可移植性下降。
memset 初始化
int a[10] = {0}; //初始化为 0
或者使用memset
int a[10];
memset(a, 0, sizeof(a));
- 分析:memset 第 1 个参数是内存起始地址,第 2 个参数是要设置的值,第 3 个参数是单位为字节的内存大小。
第 6 章 函数
getchar()
函数原型为
int getchar(void);
可以看出返回值为 int
。
函数规则
- 复杂的函数中,在分支语句、循环语句结束之后需要适当的注释,方便区分各分支或循环体。
//end "for(condition)"
//end "if(condition)"
//end "while(condition)"
- 代码行最大长度宜控制在
80
个字符以内,较长的语句、表达式等要分成多行书写。 - 函数体的规模要小,尽量控制在
80
行代码以内。 - 函数的 static 局部变量是函数的“记忆”存储器,建议尽量少用 static 局部变量。
- 参数个数尽量控制在 4 个或 4 个以内。
函数递归
一个简单但易出错的递归例子
#include "stdio.h"
void fun(int i)
{
if(i>0)
{
fun(i/2);
}
printf("%d\n", i);
}
int main()
{
fun(10);
return 0;
}
- 运行结果:
0
1
2
5
10 - 分析:最内层的最先输出
不使用任何变量编写 strlen 函数
int my_strlen(const char *strDest)
{
assert(NULL != strDest); //参数入口校验
return ('\0' != *strDest) ? (1 + my_strlen(strDest+1)) : 0;
}
- 分析:尽量不要使用递归,也要注意递归的层次不要太深,防止出现栈溢出的错误,同时要注意递归的停止条件一定要正确。
第 7 章 文件结构
需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。
第 8 章 关于面试的秘密
- 学生就是学生,穿着符合自己身份就行了
- 谈吐要符合自己身份,切忌不懂装懂、满嘴胡咧咧
- 态度是一种习惯,习惯决定一切,忌浮躁、忌傲慢、忌眼高手低
- 要学会尊敬别人和懂礼貌
让考官眼前一亮的简历
- 自我介绍一定要简明而不简单、突出重点、突出你的长处、突出你和别人不一样的地方,争取一个好的第一印象。
- 个人信息不用写身高、体重,不要写错学校相关信息,不要泄露自己的家庭电话和身份证号等个人信息
- 不要硬件、软件都写,让人觉得你都不行,不要写精通,要写掌握、熟练掌握,写上你看过的教材之外的一些经典专业书籍,面试一般不会超出你简历上所写的内容,而且会挑你最熟悉的知识问。
- 成绩表是应届生必须要准备的