C语言常见易忘易忽略的知识点
1、大小端问题
数据存储方向
大端: 数据 低 位:存储在 地址 高 位
小端: 数据 低 位:存储在 地址 低 位
需要考虑的场合
数据存储方向,包括在socket网络通信中涉及到的字节序问题
PC(X86-英特尔):小端
嵌入式设备(ARM):可通过修改底层寄存器的值设定大小端,默认小端
代码验证
采用**共用体(union)**的特性:
结构中的各成员共享内存空间
#include <stdio.h>
typedef unsigned int u32;
typedef unsigned char u8;
union {
u32 i;
u8 x[4];
} a;
u32 main(){
// a.x[0] = 0; // 16进制,4it
// a.x[1] = 1;
// a.x[2] = 2;
// a.x[3] = 3;
printf("sizeof(u32) : %d\nsizeof(u8) : %d\n",\
sizeof(u32), sizeof(u8));
printf("sizeof(a.x[0]) : %d\n\n", sizeof(a.x[0]));
a.i = 0x12345678;
// x[ -3 -2 -1 -0]
// 0x 12 34 56 78
printf("(int) : %x\n", a.i);
printf("addr0 : %x\naddr1 : %x\n", a.x[0], a.x[1]);
printf("addr2 : %x\naddr3 : %x\n", a.x[2], a.x[3]);
return 0;
}
结果:
addr3 : 12
PS D:\...> cd "d:\...\" ; if ($?) { gcc bledge.c -o bledge } ; if ($?) { .\bledge }
sizeof(u32) : 4
sizeof(u8) : 1
sizeof(a.x[0]) : 1
(int) : 12345678
addr0 : 78
addr1 : 56
addr2 : 34
addr3 : 12
2
PS D:\...>
2、最大公约数与最小公倍数
1、最小公倍数 = 输入两数之积 除以 它们的最大公约数。
2、最大公约数 :辗转相除法。
- 辗转相除法原理:
较大数 除以 较小数,
以 除数 和 余数 反复做除法运算,
当 余数 = 0 时,取 当前算式除数 为最大公约数。
代码验证
#include <stdio.h>
typedef unsigned int u32;
u32 Gcd(u32 M,u32 N)
{
u32 Rem;
// 不必考虑除数和被除数大小的问题
// (整数情况下)通过取余可以直接实现互换
while(N)
{
Rem = M % N;
M = N;
N = Rem;
}
return M;
}
int main(void)
{
int a,b;
scanf("%d %d",&a,&b);
printf("MaxCommonFactor of %d and %d is ",a,b);
printf("%d\n",Gcd(a,b));
return 0;
}
// 结果:
PS D:\...> cd "d:\...\" ; if ($?) { gcc algo_jiushao.c -o algo_jiushao } ; if ($?) { .\algo_jiushao }
12 128
MaxCommonFactor of 12 and 128 is 4
3、字符串赋值
void fun(char *a, char *b) {
while((*b=*a)!='\0') {
a++;
b++;
}
}
char n[10] = "hello";
char m[10] = "world567";
// char *a = &m; //warning
// char *b = &n;
// warning: initialization of 'char *'
// from incompatible pointer type 'char (*)[10]'
// [-Wincompatible-pointer-types]
char *a = m; // well
char *b = n;
fun(a, b);
4、static只被初始化1次
不管是 局部 还是 全局。
#include "stdio.h"
int main(){
int initNum = 3;
for (int i=5; i > 0; --i) {
static int n1 = initNum;
n1++;
printf("%d\n", n1);
}
return 0;
}
// 输出结果:
// 4
// 5
// 6
// 7
// 8
虽然代码循环了5次,静态变量n1确实只初始化了1次。
#include <stdio.h>
int main(){
int initNum = 3;
for (int i=5; i > 0; --i)
{
// p.s: 如今的static变量只能由常量进行初始化
static int n1 = initNum;
// 添加代码 将比 静态变量地址 +1 的数据置0;
int* p = &n1;
p++;
// ->
*p = 0;
//end
n1++;
printf("%d\n", n1);
}
return 0;
}
// 输出结果:
// 4
// 4
// 4
// 4
// 4
n1 03 00 00 00 (低址->高址)
*p 01 00 00 00 (低址->高址)
其实静态变量通过静态变量后面的一个32位内存位来做记录,以标识这个静态变量是否已经初始化。
而p++; *p = 0;
却每次都将这个值赋值为0
所以程序就一直认为n1一直没有被初始化过,并每次都初始化一次。
代码中之所以要用int initNum = 3;
而不是直接用static int n1 = 3;
是因为如果给静态变量直接 赋值一个常量的话,
编译器会进行优化,
导致程序在一启动时就初始化完毕,不便于观察静态变量内存上的改变。
(然而现在的静态变量不允许使用变量进行初始化了)
5、奇怪的短路原则
6、strcpy的等空间赋值
7、结构体变量名:值传递
#include <stdio.h>
#include <string.h>
struct A{
int a;
char b[2];
double c;
};
// 有返回值(不需要对母函数中变量进行修改,仅需传值)
struct A f(struct A t);
// 无返回值(需要对母函数中变量进行修改,要传地址)
void fptr(struct A *t);
int main(){
struct A a = {1001, "OK", 1.23};
//f(a); //不会影响 a 中的情况
a = f(a); //返回值更新了原结构体变量中的成员值
printf("%d, %s, %lf\n", a.a, a.b, a.c);
fptr(&a); //地址传递直接更新原结构体变量中的成员值
int x = 20;
// 奇怪的短路原则
printf("shortcut1 : %d\n", 0<x<20); //返回1
printf("shortcut2 : %d\n", 0<x&&x<20); // 返回0
return 0;
}
struct A f(struct A t){
t.a = 4004;
strcpy(t.b, "No");
t.c = 2.12;
return t;
}
void fptr(struct A *t){
// 结构体对象传值
// 地址指向为int变量:直接赋值
t->a = 9999;
// 字符数组为指针
strcpy(t->b, "PS");
// 地址指向为double变量:直接赋值
t->c = 0.01;
}
// 结果:
// 4004, No, 2.120000
// 9999, PS, 0.010000
// shortcut1 : 1
// shortcut2 : 0 //以后发现无法显示,请先在头部回车
8. 约瑟夫环
问题:
一个升序n项的循环链表, (1, 2, 3, …, n)
设起点值为k
要求进行如下循环操作: 删除第m个元素
例:(n = 6, k = 4, n = 3)
原始状态: 1,2,3,4,5,6
1起始点: 4
1更新状态:1,2,3,4,5 (因为m(4) = 1, m(5) = 2, m(6) = 3, 故删除6)
2更新起始点: 1
2更新状态:1,2,4,5 (删除3)
3更新起始点: 4
3更新状态:2,4,5 (删除1)
4更新起始点: 2
4更新状态:2, 4 (删除5)
5更新起始点: 2 (删除2)
5更新状态:4
剩余状态:4
删除顺序: 6, 3, 1, 5, 2, 4
#include <stdio.h>
#include <stdlib.h>
typedef struct node {
int data;
struct node *next;
} Node;
Node *circle_create(int n);
void count_off(Node *head, int n, int k, int m);
int main() {
int n, k, m;
scanf("%d%d%d", &n, &k, &m);
Node *head = circle_create(n);
count_off(head, n, k, m);
return 0;
}
Node *circle_create(int n) {
Node *temp, *new_node, *head;
int i;
// 创建第一个链表节点并加数据
temp = (Node *) malloc(sizeof(Node));
head = temp;
head->data = 1;
// 创建第 2 到第 n 个链表节点并加数据
for(i = 2; i <= n; i++) {
new_node = (Node *) malloc(sizeof(Node));
new_node->data = i;
temp->next = new_node;
temp = new_node;
}
// 最后一个节点指向头部构成循环链表
temp->next = head;
return head;
}
void count_off(Node *head, int n, int k, int m) {
Node *temp, *pre;
temp = head;
pre = head;
int i;
// 1. 找到起始值
while(temp->data != k) {
temp = temp->next;
}
// 2. 初始化pre(为了 m = 1做准备)
while(pre->data != n) {
pre = pre->next;
}
while(temp != temp->next) {
// 为除了m != 1之外的情况初始化pre
for (i = 1; i < m; i++) {
pre = temp;
temp = temp->next;
}
// 前后指针的删除操作
pre->next = temp->next; // 将待删项的前一点指向待删点的后一点
printf("%d ", temp->data);
free(temp); // 防止ML
temp = pre->next; // 此时的pre仍是已删点的前一点!故新起点为pre->next
}
printf("%d\n",temp->data);
return;
}
9. 枚举显性编号操作
默认编号从0开始:
enum week{
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
} Week;
int main() {
Week meeting_date;
meeting_date = FRIDAY;
printf("%d\n", meeting_date);
return 0;
// 5
// SUNDAY = 0;
// MONDAY = 1;
// SATURDAY = 6;
可以手动设置起始编号:
enum week{
SUNDAY = 4,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
} Week;
int main() {
Week meeting_date;
meeting_date = FRIDAY;
printf("%d\n", meeting_date);
return 0;
// 9
// SUNDAY = 4;
// MONDAY = 5;
// SATURDAY = 10;
可以同时设置两个起始编号:
enum week{
SUNDAY = 4,
MONDAY,
TUESDAY,
WEDNESDAY = 4,
THURSDAY,
FRIDAY,
SATURDAY
} Week;
int main() {
Week meeting_date;
meeting_date = FRIDAY;
printf("%d\n", meeting_date);
return 0;
// 6
// SUNDAY = 4;
// MONDAY = 5;
// SATURDAY = 7;
10、巧用(函数指针)代替(数组)进行二分查找
例题:
三角数列递推公式:
Tn = n * (n + 1) / 2
五角数列递推公式:
Pn = n * (3 * n - 1) / 2
六角数列递推公式:
Hn = n * (2n - 1)
求数x同时为三角、五角、六角数
#include <stdio.h>
typedef long long ll;
ll triangle(int n) {
return (n * (n + 1) / 2);
}
ll Pentangon(int n) {
return (n * (3 * n - 1) / 2);
}
ll Hexangon(int n) {
return (n * (2 * n - 1));
}
// ll binary_search(ll *arr, ll n, ll x)
ll binary_search(ll (*fun)(ll), ll n, ll x) {
ll head = 1, tail = n - 1, mid;
while (head <= tail) {
mid = (head + tail) >> 1;
// if (arr[mid] == x)...
if (fun(mid) == x) return mid;
if (fun(nid) < x) head = mid + 1;
else tail = mid - 1;
}
return -1;
}
int main() {
ll n = 1;
while (1) {
n += 1;
ll temp = Triagle(n);
if (binary_search(Pentangon, n, temp) == -1) continue;
if (binary_search(Hexangon, n, temp) == -1) continue;
printf("%d %d\n", n, temp);
break;
}
return 0;
}
11. 函数重载
两个名字相同的函数必须具有不同(个数或类型)的形参
是否可以重载与函数的返回值 和 形参的名称无关
若函数名称 和 形参类型都相同的话, 则编译错误 (函数重复定义)
p.s.:
对于有默认形参的函数重载应避免二义性:函数重载
void fun(int len, int wid = 2, int hei = 3);
void fun(int len);
二义性问题
fun(1);
12. 多文件编译为何要分头文件(.h),函数库文件(.cc)和主函数文件(.c)?
条件编译的作用是防止多次定义头文件中只允许声明是因为若同时定义则可能导致多个头文件产生的重定义错误
(通常.cc文件放在src目录下,.h文件放在include目录下)