1.位运算
1.1位逻辑运算符
4个位运算符用于整型数据,包括char.将这些位运算符成为位运算的原因是它们对每位进行操作,而不影响左右两侧的位,常规的位的逻辑运算符对整个值进行操作。
按位取反~:一元运算符 ~ 将每个1变为0,将每个0变为1,
~(10011010)
01100101
位与(AND)&:二进制运算符&通过对两个操作数逐位进行比较产生一个新值。对于每个位,只有两个操作数的对应位都是1时结果才为1。
(10010011)
& (00111101)
= (00010001)
C也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果:
val &= 0377
val = val & 0377
位或(OR) |:二进制运算符|通过对两个操作数逐位进行比较产生一个新值。对于每个位,如果其中任意操作数中对应的位为1,那么结果位就为1.
(10010011)
| (00111101)
= (10111111)
C也有组合位或-赋值运算符: |=
val |= 0377
val = val | 0377
位异或^
二进制运算符^对两个操作数逐位进行比较。对于每个位,如果操作数中的对应位有一个是1(但不都是1),那么结果是1。如果都是0或者都是1,则结果位0.
(10010011)
^ (00111101)
= (10101110)
C也有一个组合的位异或-赋值运算符: ^=
val ^= 0377
val = val ^ 0377
TipS:打开位:或|(或反|~ ) ,关闭位:与反&~
转置位用位异或运算符来转置
1.2移位运算符
左移 <<
左移运算符<<将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用0填充,并且丢弃移出左侧操作数末端的位。在下面例子中,每位向左移动两个位置。
(10001010) << 2
(00101000)
右移 >>
右移运算符>>将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用0填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用0填充,或者使用符号(最左端)位的副本填充。
//有符号值
(10001010) >> 2
(00100010) //在某些系统上的结果值
(10001010) >> 2
(11100010) //在另一些系统上的结果
//无符号值
(10001010) >> 2
(00100010) //所有系统上的结果值
number << n | number乘以2的n次幂 |
---|---|
number >> n | 如果number非负,则用number除以2的n次幂 |
2.多维数组
2.1一维数组
元素类型角度:数组是相同类型的变量的有序集合
内存角度:连续的一大片内存空间
在C中,在几乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向int的常量指针”.
当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。
当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。
*(arr + 3)表示arr指针向后移动了3个元素的长度,然后通过间接访问操作符从这个新地址开始获取这个位置的值。(arr + 3)等价于arr[3]
2.2 数组和指针
int a[10];
int *b;
声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本身分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。
因此,表达式a是完全合法的,但是表达式b却是非法的。*b将访问内存中一个不确定的位置,将会导致程序终止。另一方面b++可以通过编译,a++却不行,因为a是一个常量值。
作为函数参数的数组名:当一个数组名作为一个参数传递给一个函数, 数组名其实就是一个指向数组第1个元素的指针,所以很明白此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针, 为什么一维数组中无须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。
int print_array(int *arr);
int print_array(int arr[]);
2.3多维数组
指向数组的指针(数组指针)
数组指针,它是指针,指向数组的指针。
数组的类型由元素类型和数组大小共同决定:int array[5] 的类型为 int[5];C语言可通过typedef定义一个数组类型:
定义数组指针有三种方式:
//方式一
void test01(){
//先定义数组类型,再用数组类型定义数组指针
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
//有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType
typedef int(ArrayType)[10];
//int ArrayType[10]; //定义一个数组,数组名为ArrayType
ArrayType myarr; //等价于 int myarr[10];
ArrayType* pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
for (int i = 0; i < 10;i++){
printf("%d ",(*pArr)[i]);
}
printf("\n");
}
//方式二
void test02(){
int arr[10];
//定义数组指针类型
typedef int(*ArrayType)[10];
ArrayType pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}
//方式三
void test03(){
int arr[10];
int(*pArr)[10] = &arr;
for (int i = 0; i < 10; i++){
(*pArr)[i] = i + 1;
}
for (int i = 0; i < 10; i++){
printf("%d ", (*pArr)[i]);
}
printf("\n");
}
指针数组(元素为指针)
栈区指针数组
//数组做函数函数,退化为指针
void array_sort(char** arr,int len){
for (int i = 0; i < len; i++){
for (int j = len - 1; j > i; j --){
//比较两个字符串
if (strcmp(arr[j-1],arr[j]) > 0){
char* temp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = temp;
}}}}
//打印数组
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
void test(){
//主调函数分配内存
//指针数组
char* p[] = { "bbb", "aaa", "ccc", "eee", "ddd"};
//char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //错误
int len = sizeof(p) / sizeof(char*);
//打印数组
array_print(p, len);
//对字符串进行排序
array_sort(p, len);
//打印数组
array_print(p, len);
}
堆区指针数组
//分配内存
char** allocate_memory(int n){
if (n < 0 ){
return NULL;
}
char** temp = (char**)malloc(sizeof(char*) * n);
if (temp == NULL){
return NULL;
}
//分别给每一个指针malloc分配内存
for (int i = 0; i < n; i ++){
temp[i] = malloc(sizeof(char)* 30);
sprintf(temp[i], "%2d_hello world!", i + 1);
}
return temp;
}
//打印数组
void array_print(char** arr,int len){
for (int i = 0; i < len;i++){
printf("%s\n",arr[i]);
}
printf("----------------------\n");
}
//释放内存
void free_memory(char** buf,int len){
if (buf == NULL){
return;
}
for (int i = 0; i < len; i ++){
free(buf[i]);
buf[i] = NULL;
}
free(buf);
}
void test(){
int n = 10;
char** p = allocate_memory(n);
//打印数组
array_print(p, n);
//释放内存
free_memory(p, n);
}
小结:
- 在绝大多数表达式中,数组名的值是指向数组第1个元素的指针。这个规则只有两个例外,sizeof和对数组名&。
- 指针和数组并不相等。当我们声明一个数组的时候,同时也分配了内存。但是声明指针的时候,只分配容纳指针本身的空间。
- 当数组名作为函数参数时,实际传递给函数的是一个指向数组第1个元素的指针。我们不单可以创建指向普通变量的指针,也可创建指向数组的指针。
3.结构体
结构体类型的定义
struct Person{
char name[64];
int age;
};
typedef struct _PERSON{
char name[64];
int age;
}Person;
结构体变量的定义
struct Person{
char name[64];
int age;
}p1; //定义类型同时定义变量
struct{
char name[64];
int age;
}p2; //定义类型同时定义变量
struct Person p3; //通过类型直接定义
结构体变量的初始化
struct Person{
char name[64];
int age;
}p1 = {"john",10}; //定义类型同时初始化变量
struct{
char name[64];
int age;
}p2 = {"Obama",30}; //定义类型同时初始化变量
struct Person p3 = {"Edward",33}; //通过类型直接定义
结构体成员的使用
struct Person{
char name[64];
int age;
};
void test(){
//在栈上分配空间
struct Person p1;
strcpy(p1.name, "John");
p1.age = 30;
//如果是普通变量,通过点运算符操作结构体成员
printf("Name:%s Age:%d\n", p1.name, p1.age);
//在堆上分配空间
struct Person* p2 = (struct Person*)malloc(sizeof(struct Person));
strcpy(p2->name, "Obama");
p2->age = 33;
//如果是指针变量,通过->操作结构体成员
printf("Name:%s Age:%d\n", p2->name, p2->age);
}
注意:定义结构体类型时不要直接给成员赋值,结构体只是一个类型,编译器还没有为其分配空间,只有根据其类型定义变量时,才分配空间,有空间后才能赋值。
结构体赋值
相同的两个结构体变量可以相互赋值,把一个结构体变量的值拷贝给另一个结构体,这两个变量还是两个独立的变量。
深拷贝和浅拷贝
//一个老师有N个学生
typedef struct _TEACHER{
char* name;
}Teacher;
void test(){
Teacher t1;
t1.name = malloc(64);
strcpy(t1.name , "John");
Teacher t2;
t2 = t1;
//对手动开辟的内存,需要手动拷贝
t2.name = malloc(64);
strcpy(t2.name, t1.name);
if (t1.name != NULL){
free(t1.name);
t1.name = NULL;
}
if (t2.name != NULL){
free(t2.name);
t1.name = NULL;
}
}
结构体数组
struct Person{
char name[64];
int age;
};
void test(){
//在栈上分配空间
struct Person p1[3] = {
{ "John", 30 },
{ "Obama", 33 },
{ "Edward", 25}
};
struct Person p2[3] = { "John", 30, "Obama", 33, "Edward", 25 };
for (int i = 0; i < 3;i ++){
printf("Name:%s Age:%d\n",p1[i].name,p1[i].age);
}
printf("-----------------\n");
for (int i = 0; i < 3; i++){
printf("Name:%s Age:%d\n", p2[i].name, p2[i].age);
}
printf("-----------------\n");
//在堆上分配结构体数组
struct Person* p3 = (struct Person*)malloc(sizeof(struct Person) * 3);
for (int i = 0; i < 3;i++){
sprintf(p3[i].name, "Name_%d", i + 1);
p3[i].age = 20 + i;
}
for (int i = 0; i < 3; i++){
printf("Name:%s Age:%d\n", p3[i].name, p3[i].age);
}
}
结构体成员偏移量
结构体字节对齐
内存的最小单元是一个字节,当cpu从内存中读取数据的时候,是一个一个字节读取。
提高存取数据的速度。比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。
某些平台只能在特定的地址处访问特定类型的数据,否则抛出硬件异常给操作系统。
对于标准数据类型,它的地址只要是它的长度的整数倍。
对于非标准数据类型,比如结构体,要遵循一下对齐原则:
- 数组成员对齐规则。第一个数组成员应该放在offset为0的地方,以后每个数组成员应该放在offset为min(当前成员的大小,#pargama pack(n))整数倍的地方开始(比如int在32位机器为4字节,#pargama pack(2),那么从2的倍数地方开始存储)。
- 结构体总的大小,也就是sizeof的结果,必须是min(结构体内部最大成员,#pargama pack(n))的整数倍,不足要补齐。
- 结构体做为成员的对齐规则。如果一个结构体B里嵌套另一个结构体A,还是以最大成员类型的大小对齐,但是结构体A的起点为A内部最大成员的整数倍的地方。(struct B里存有struct A,A里有char,int,double等成员,那A应该从8的整数倍开始存储。),结构体A中的成员的对齐规则仍满足原则1、原则2。
手动设置对齐模数:
#pragma pack(show)
显示当前packing alignment的字节数,以warning message的形式被显示。
#pragma pack(push)
将当前指定的packing alignment数组进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数组压栈。
#pragma pack(pop)
从internal compiler stack中删除最顶端的reaord; 如果没有指定n,则当前栈顶record即为新的packing alignement数值;如果指定了n,则n成为新的packing alignment值
#pragma pack(n)
指定packing的数值,以字节为单位,缺省数值是8,合法的数值分别是1,2,4,8,16。
#pragma pack(4)
typedef struct _STUDENT{
int a;
char b;
double c;
float d;
}Student;
typedef struct _STUDENT2{
char a;
Student b;
double c;
}Student2;
void test01(){
//Student
//a从偏移量0位置开始存储
//b从4位置开始存储
//c从8位置开始存储
//d从16位置开存储
//所以Student内部对齐之后的大小为20 ,整体对齐,整体为最大类型的整数倍 也就是8的整数倍 为24
printf("sizeof Student:%d\n",sizeof(Student));
//Student2
//a从偏移量为0位置开始
//b从偏移量为Student内部最大成员整数倍开始,也就是8开始
//c从8的整数倍地方开始,也就是32开始
//所以结构体Sutdnet2内部对齐之后的大小为:40 , 由于结构体中最大成员为8,必须为8的整数倍 所以大小为40
printf("sizeof Student2:%d\n", sizeof(Student2));
}