C进阶

数字进制

权重

某一位上的数字可以表示为有几个权重,不同的位上每一个的权重不同

十进制的某一位上最多只能有9个,最少0个
二进制的某一位上最多只能有1个,最少0个

进位就是当前位的个数超过最大值后需要清零,并且下一位的个数加一

比如,十进制的124

  • 第一位上的一个的权重为10^0 ,第一位有4个,所以第一位上的权重和就是4* 10^0
  • 第二位上的一个的权重为10^1 ,第二位上有2个,所以第二位上的权重和就是2* 10^1
  • 第三位同理,第三位的权重和就是1* 10^2
  • 所有位的权重和就是4 *10^0 + 2* 10^1 +1* 10^2

比如,二进制的011

  • 第一位上的一个的权重为2^0 ,第一位有1个,所以第一位上的权重和就是1* 2^0
  • 第二位上的一个的权重为2^1 ,第二位有1个,所以第二位上的权重和就是1* 2^1
  • 第三位上的一个的权重为2^2 ,第二位有0个,所以第三位上的权重和就是0* 2^2
  • 所有位的权重和就是1* 2^0 +1* 2^1 +0* 2^2

用二进制和十进制表示的所有位的权重和本质是一样的(都是同一个数)

二进制转十进制

由于二进制和十进制用权重和表示的都是同一个数,所以
计算机世界(二进制)的011放在人类世界(十进制)就是0+2+1=3

二进制转十进制就直接表示出二进制的所有位的权重和,然后再把权重和计算成十进制

对于小数的二进制,小数第n位占的权值为\(2^{-n}\)

十进制转二进制

\(2^0\) 1
\(2^1\) 2
\(2^2\) 4
\(2^3\) 8
\(2^4\) 16
\(2^5\) 32
\(2^6\) 64
\(2^7\) 128
\(2^8\) 256
\(2^9\) 512
\(2^{10}\) 1024

思路:从上表中选择2^n 来组合成十进制数(从大到小找),n表示二进制的1后面有n个0

比如:十进制的1000转二进制

  • 可以贡献512(十进制)的权值,二进制就是1后面9个0:10 0000 0000,还差488(十进制)的权值
  • 可以贡献256(十进制)的权值,二进制就是1后面8个0:11 0000 0000,还差232(十进制)的权值
  • 可以贡献128(十进制)的权值,二进制就是1后面7个0:11 1000 0000,还差104(十进制)的权值
  • 可以贡献64(十进制)的权值,二进制就是1后面6个0:11 1100 0000,还差40(十进制)的权值
  • 可以贡献32(十进制)的权值,二进制就是1后面5个0:11 1110 0000,还差8(十进制)的权值
  • 可以贡献8(十进制)的权值,二进制就是1后面3个0:11 1110 1000,权值够了
  • 最后得到二进制:11 1110 1000

对于小数的十进制转二进制,小数部分和整数部分分开算,小数部分乘2取整得到小数的第一位,反复直到小数部分为0

八进制和十六进制

八进制和十六进制就是为了简化二进制的表示

八进制:二进制的三位表示八进制的一位

十六进制:二进制的四位表示十六进制的一位

比如一个二进制数:100110

  • 用十六进制来看,每四位组成十六进制的一位,第一位0110就是6,第二位10就是2,所以用十六进制表示就是26
  • 用八进制来看,每三位组成八进制的一位,第一位110就是6,第二位100就是4,所以用八进制来表示就是46

统计二进制表示中1的个数

Brian Kernighan算法

对一个整数x,对其x=x&(x-1),直到x为0,运算的次数就是x的二进制表示法中1的个数

int countOnes(int x){
	int cnt=0;
	while(x>0){
		x&=(x-1);
		cnt++;
	}
	return cnt;
}

C中的进制

C中表示数字默认是十进制,表示八进制在数字前加0,表示十六进制在数字前面加0x

输出十进制用%d,输出八进制用%o,输出十六进制用%x,输入同理

C的格式化输入输出实际上就是在不同的世界(计算机世界,人类世界)表示同一个数,不同的进制表示的所有位的权重和是一样的,计算机内部其实是用二进制来储存这个数

用数字表示状态

数字不仅可以有算数意义,还可以有状态意义,数字n可以表示有n种不同的状态或者是某一种具体的第n种状态

把不同状态具体化为不同的字符或颜色,就可以构造字符(或颜色)和数字的映射关系

ASCⅡ表

与不同进制的表示法一样,字符也是一种表示法,本质仍然是数字,计算机处理字符的表示的时候会根据ASC表把数字映射成对应的字符

为了表示ASC表中的不可视字符,可以用\转义字符

比如:
'\n':换行
'\r':回车
'\t':制表符
'\数字':数字表示的字符,只能是八进制
特别的:'\0':字符串的结束标志

signed和unsigned

  • signed和unsigned是修饰词,并不影响空间大小
  • signed表示数字有数学意义(有正负)
  • unsigned表示数字只是单纯的数据(没有正负)
  • 只约束空间访问行为,不约束空间容量属性,主要表现在赋值和乘除,逻辑运算,有符号数和无符号数的比较会降级为两个无符号数的比较
    int a=-10;
    unsigned int b=10;
    if(a>b)cout<<"true"<<endl;
    else if(a<b)cout<<"false"<<endl;
    
    结果为true

解码器

原码,反码,补码用于简化计算机的数学运算,计算机中以补码形式储存(补码效率最高)
负数的二进制是负数的补码

  • 原码:直接用二进制表示一个数,最高位是符号位,1表示负数,0表示正数
  • 反码:正数的反码和原码相同,负数的反码是原码除了符号位外其他位全部取反
  • 补码:正数的补码和原码相同,负数的补码是反码加一

原码得到补码的过程:

  1. 原码:1000 0010(十进制的-2)
  2. 反码:1111 1101(符号位不动,其他位取反)
  3. 补码:1111 1110(反码基础上加一)

%d,%u,%x的解码器分别把补码按有符号十进制,无符号十进制,无符号十六进制的方式解码成对应的数字,再转化成字符流,最后再输出到屏幕上

  • 有符号解码:补码最高位是负权值,其他位是正权值,计算所有位的权值和
  • 无符号:补码所有位都是正权值,计算所有位的权值和

赋值和乘除

  • 容量大的赋值到容量小的,只保留低位

  • 容量小的赋值到容量大的,若接受赋值的容器是有符号数,补符号位,若接受赋值的容器是无符号数,补0

  • 有符号数可以赋给无符号变量,只关心容器大小是否够用,只是小容量赋到大容量时补高位的方式是补0

  • 乘以2相当于补码所有位向左移动一位,低位补0

  • 除以2相当于补码所有位向右移动一位,若被除数是有符号数,高位补符号位,若被除数是无符号数,高位补0

数据类型

基本数据类型

基本数据类型是内存分配行为关键字

C中的几种数据类型本质是内存分配行为关键字,告诉编译器申请一块内存用来存数据

这几种容器的大小是由编译器来决定的,一般来说

char:1B
int:<=4B
long:>=4B
long long:8B
float:4B
double:8B

1B:0-255
2B:0-65535

标准:char<short<=int<=long<=long long

C中表示的整型一般默认是有符号的int的大小,不同机器int大小可能不一样(编译器决定),用两个字节的大小放一个超过两个字节的整形,高位会被截断只保留低位,为了保证可移植性,在整形常量后面加上L后缀,表示这是一个long类型,至少有4B,要表示无符号的整形常量需要在数字后面加U后缀

C中表示的浮点型默认是double的大小,用一个浮点型常量(默认是double)给一个float赋值,会丢失精度,所以在表示float大小的浮点型常量后面加上f后缀

自定义数据类型

  • 累加组合:struct,struct中的成员的空间是相邻的,结构体对齐按照成员最大的数据类型作为对齐标准

    struct abc{
    	char a1;
    	int c3;
    	short b2;
    };
    int main(){
    	//结果是12,因为按照int作为对齐标准
    	printf("size of abc=%zu",sizeof(struct abc));
    	return 0;
    }
    
  • 共享组合:union,本质是给不同大小的空间命名(成员),所有成员的空间的起点都一样,简化了指针操作

C编译

源文件从编译到汇编,在到链接生成可执行文件

预处理器

源文件->预处理文件
gcc -E -o a.i a.c

预处理:#开头

  1. 包含头文件,如include <stdio.h>
  2. 宏定义:替换#define 宏体 宏名,宏名会替换宏体
  3. 条件编译,可以用于调试:
    #ifdefine _DEBUG
    printf("hello\n");
    #else
    printf("world\n");
    #endif
    
    终端编译时:
    gcc -D_DEBUG -o a.exe a.c
  4. 删除注释

编译器

预处理文件->汇编语言文件
gcc -S -o a.s a.i

汇编器

汇编语言文件->机器指令集
gcc -c -o a.o a.s

链接器

机器指令集->可执行文件
gcc -o a.exe a.o

编译确定地址偏移量,链接确定地址

nm指令

nm 目标文件一般作用于.o文件,显示符号映射表

  • U:未定义符号,最后链接时确定这个符号的地址
  • T:代码段符号
  • D:数据段符号(data),全局,可被链接
  • d:数据段符号(data),局部,不可被链接
  • B:数据段符号(bss),未初始化,全局,可被链接
  • b:数据段符号(bss),未初始化,局部,不可被链接

指定C++版本

g++ -std指令

  • -std=c++98
  • -std=c++99
  • -std=c++11
  • -std=c++14
  • -std=c++17

函数

C中用函数描述一个过程

  • 函数名本质是存放函数代码空间的地址的别名,与数组名一样,只是个符号,对函数名取地址没有意义
  • 函数指针是特殊的地址,可以接受信息(参数个数,参数类型),可以返回信息(返回值类型)
  • 可以定义一个函数指针变量来保存不同的函数指针常量
  • 用typedef给函数指针起别名:
    typedef void (*myfunct)(int,int);目的是方便阅读

传参

  • 传参的本质是从调用者处拷贝值到函数空间(栈区),哪怕是传了一个指针,也是把指针的值(地址)拷过来
  • 由于内存地址是唯一的,所以可以通过地址操作函数空间外的空间,要更新某个空间就传这个空间的地址
  • 要传入一个占空间比较大的值时,可以直接传这个值所在空间的地址,只需要拷贝地址,不用拷贝整个空间,如果只需要对这个空间读,可以用const修饰指针
void funct1(int);    //对基础数据类型只读
void funct2(int*);    //对基础数据类型可写
void funct3(const int*,int num);    //对int数组只读,num表示数组单元个数,避免越界
void funct4(int*,int num);    //对int数组可写,num避免越界
void funct5(void*,size_t n);    //对不确定类型的连续空间可写,n表示以字节为单位的个数
  • 若要忽略字符空间的结束标志'\0'(本质是0),只把0当作数据来看,可以在声明函数时用void *作为参数类型,只需要接收一个地址,不关心访问行为,函数内部再按照某种方式处理这个地址所指的空间,由于不再把这个空间当作字符空间,就没有了默认的结束标志'\0',需要给定num界定空间大小(num个字节)避免越界
    void funct6(const void *,int num);    //对数据空间只读
    void funct7(void *,int num);    //对数据空间可写
    
    void funct8(void *src,int num){
    	int* p=(int*)src;    //转成需要的指针类型进行处理
    	//......
    }
    
  • 函数参数是倒着入栈的,编译器会先处理后面的参数
    int main(){
    	int arr[] = {6,7,8,9};
    	int* ptr=arr;
    	//结果是7 7,先处理*(++ptr)
    	printf("%d %d\n",*ptr,*(++ptr));
    	return 0;
    }
    

返回值

  • 返回值本质仍然是拷贝
  • 函数返回值后,函数内的数据就都不存在了,所以不能返回栈区的地址,返回堆区的地址要注意释放问题比如fopen开空间,fclose释放空间

空间处理

指针也是一种变量,本质仍然是储存数据的盒子,只不过储存的数据是一个地址

访问内存空间的方法

有名访问

int a; 定义一个变量a,实际上是在内存上开了一块int大小的空间,为了让cup访问到这块空间,仍然需要让cup知道这块空间的地址,所以变量名a就是这个地址的代号,可以直接通过地址的代号访问

无名访问

直接通过地址访问内存空间

指针的要素

1.指针的大小

指针的大小只与编译器目标操作系统的位数有关,32位的操作系统指针大小就是4B,64位的操作系统指针大小就是8B

2.指针访问的跨度(访问方式)

指针从一个单位到下一个单位访问的跨度大小就是指针所指的这块内存所表示的数据类型的大小,值相等的两个指针访问方式可能不同,指针加减整数是偏移整数个跨度单位

int* p; 定义一个指针p,p的大小由操作系统决定,int规定了p访问的跨度是int大小

指针的声明

先定位变量名,然后向右看,再向左看

  • int (*k)[4];先定位指针名k,然后向右看没有,向左看有*,k升级为指针,但还不确定访问方式,再向右看[ 4 ],再向左看int,访问方式是4个int4个int地访问
  • int *a[5];先定位指针名a,然后向右看是[ 5 ],a升级为一个数组,再向左看是*,每个数组单元是一个指针,但还不确定访问方式,再向右看没有,再向左看是int,说明每个指针的访问方式是一个int一个int地访问
  • C中声明一个二维数组即便不指定行数也必须指定列数原因在于“列数”规定了指针的访问方式
    int a[3][4];4表示指针a的访问方式是4个int4个int地访问,一共有3个元素
  • int a[3][4];和int (*p)[4];的访问方式一样,同理int a[5][3][4];和int (*p)[3][4];的访问方式一样
  • char *const pp遇到const变为只读的,再遇到* 升级为只读的指针,再遇到char升级为只读的访问跨度为一个char的指针
  • const char *pp遇到*升级为指针,再遇到char升级为访问跨度为一个char的指针,再遇到const升级为指向的空间只读的访问跨度为一个char的指针,char const *p与之同理

数组名

数组名本质是数组首地址的助记符,本身不占有空间(指针是占有空间的),数组名在编译时期就确定了数组的大小,所以用sizeof得到的是整个数组的大小

  • int a[5];,那么sizeof(a)就等于5个sizeof(int)
  • 而对指针用sizeof的到的是指针的大小

编译时期把数组名转换成地址

给函数传数组名,数组名会退化成指针(不含数组大小的信息),所以本质上是传了个指针

void funct(int a[5]){
	printf("size of a: %zd\n",sizeof a);
}

得到的是指针的大小

对数组名取地址是在编译时期的行为(运行时期数组名不存在空间),得到的地址具有数组大小的信息(访问跨度),是编译器规定的意义

int a[5];
int (*p1)[5] = &a;    //p1的访问跨度是a数组的大小
int *p2 = a;    //p2的访问跨度是a数组单元的大小

函数指针

函数也是一块空间,函数名在编译时期转换成常量指针,函数指针也有指针的两个要素:大小和访问方式

  • int funct(void)定位指针名funct,向右看(void),向左看int,规定了的funct指针的访问方式是:参数void,返回值int的函数
  • int (*myfunct)(void)的访问方式和 int funct(void)的访问方式相同

应用:对于switch-case里面每个分支都要调用不同的函数,可以把这几个函数的指针保存在连续的数组中,通过下标调用指定的函数
比如函数void funct_0();void funct_1();void funct_2();,可以用数组void (*functs[3])(void);,首先定位到变量名functs,然后向右看[ 3 ],functs升级为数组,但单元类型还不确定,然后向左看是*,确定数组单元是指针,但指针的访问方式还不确定,然后向右看是(void),向左看是void,确定了functs数组单元指针的访问方式是参数为void,返回值为void的函数

空间属性

内存管理分区从高到低:

  • OS内核区:OS模式下才能访问,不可读不可写
    • 动态区:程序运行时自动产生,该区未初始化的变量默认随机值
      • 栈区:局部变量,可读可写,从函数进入到函数return,由编译器管理
      • 堆区:malloc和free,可读可写,由程序员管理,要注意内存泄漏和重复释放问题
    • 静态区:程序生成后就有
      • 数据区:全局变量和静态变量(static),可读可写,从程序执行到程序退出,常驻内存,该区未初始化的变量默认是0
      • rodata(read only data)区:常量,只读数,从程序执行到程序退出,常驻内存
      • 代码区:函数实现,只读,从程序执行到程序退出,常驻内存
  • OS保护区:不可读不可写

避免重复释放:free之后将指针设为NULL

虚拟内存分区中代码区(code)和常量区(read only data)是只读的,全局区和堆区,栈区是可读可写的,static区只在所在的函数内可读可写

比如:
char *s = "hello";,"hello"是放在常量区的,s的值是常量区的某个地址,s[0] = 'a';对"hello"常量做了写操作,是错误的
char s[] = "hello",s是放在栈区的一个字符数组,由常量区的"hello"拷贝过来初始化,栈区是可读可写的,所以s[0] = 'a';没问题

边界访问

p[10]作为左值时,方括号代表偏移量,p[10]表示向上偏移10个单位,p[-5]表示向下偏移5个单位,偏移的跨度单位取决于指针p的访问方式

  • 定义一个数据空间,需要清楚这个数据放在哪里,有什么权限,权限由系统管理

  • 定义一个数据空间,需要清楚有多少个有效空间,越界行为由程序员检查

  • C中的const修饰符只是建议,在编译阶段检查const修饰的变量除了初始化外是否在=左边,但运行阶段可以通过指针对const修饰的变量作读写操作

  • C++中的const是真正的常量,在编译阶段就保护好数据

定义边界:

  • 明确规定数量
  • 特殊的结束标志,如C中的字符串有特殊的结束标志'\0'

C字符串空间

  • C中的字符串有默认的结束标志:数字0,等价于转义字符'\0'(注意不是字符'0')
  • sizeof得到的长度是算上了隐藏的'\0',strlen得到的长度没有算上,是人看到的字符的长度
  • 使用strcpy时若dest目标数组大小不够,可能会发生越界访问,用strncpy更安全,但是若strncpy中的n比source源数组的大小(包括'\0')大,也会发生越界访问,同理,用snprintf比sprintf更安全

游程编码

游程编码是一种简单的文本压缩算法

#include <stdio.h>
#include <stdlib.h>

char* encode(unsigned char *raw_data,int *len){
	char* ret=NULL;
	//得到ret的长度
	int data_len=*len;
	int i;
	int cnt=1;
	int temp=raw_data[0];
	for(i=1;i<data_len;i++){
		if(raw_data[i]!=temp){
			cnt++;
			temp=raw_data[i];
		}
	}
	*len=cnt*2;
	ret=malloc(cnt*2);
	//printf("data len=%d\n",cnt*2);
	//exit(0);
	//填结果数组
	int index=0;
	ret[0]=raw_data[0];
	temp=raw_data[0];
	cnt=1;
	for(i=1;i<data_len;i++){
		if(raw_data[i]!=temp){
			ret[++index]=cnt;
			cnt=1;
			temp=raw_data[i];
			ret[++index]=temp;
		}
		else cnt++;
	}
	ret[++index]=cnt;
	return ret;
}

void release(char* *result){
	free(*result);
	*result=NULL;
}

int main(){
	//原始数据
	unsigned char raw_data[] = {0x11,0x11,0x11,0x11,0x22,0x22,0x33,0x33,0x33};
	//unsigned char raw_data[] = {0x11,0x22,0x33,0x55};
	//游程编码
	int len=sizeof(raw_data)/sizeof(raw_data[0]);
	char* result=encode(raw_data,&len);
	//查看数据
	int i;
	for(i=0;i<len;i++){
		printf("0x%02x  ",result[i]);
	}
	printf("\n");
	release(&result);
	return 0;
}e
posted @ 2026-10-07 16:15  mofei1116  阅读(7)  评论(0)    收藏  举报