一、位运算
可使用 C 对变量中的个别位进行操做。您可能对人们想这样作的缘由感到奇怪。这种能力有时确实是必须的,或者至少是有用的。C 提供位的逻辑运算符和移位运算符。在如下例子中,咱们将使用二进制计数法写出值,以便您能够了解对位发生的操做。在一个实际程序中,您可使用通常的形式的整数变量或常量。例如不适用 00011001
的形式,而写为 25 或者 031 或者 0x19.在咱们的例子中,咱们将使用8位数字,从左到右,每位的编号是 7 到 0。程序员
1.1 位逻辑运算符函数
4 个位运算符用于整型数据,包括 char。将这些位运算符成为位运算的缘由是它们对每位进行操做,而不影响左右两侧的位。请不要将这些运算符与常规的逻辑运算符(&& 、||和!)相混淆,常规的位的逻辑运算符对整个值进行操做。学习
1.1.1 按位取反~url
一元运算符~将每一个 1 变为 0,将每一个 0 变为 1,以下面的例子:spa
~(10011010) 01100101
假设 a 是一个unsigned char
,已赋值为 2。在二进制中,2 是00000010
.因而 -a 的值为11111101
或者 253。请注意该运算符不会改变 a 的值,a 仍为 2。.net
unsigned char a = 2; //00000010 unsigned char b = ~a; //11111101 printf("ret = %d\n", a); //ret = 2 printf("ret = %d\n", b); //ret = 253
1.1.2 位与(AND): &指针
二进制运算符 & 经过对两个操做数逐位进行比较产生一个新值。对于每一个位,只有两个操做数的对应位都是 1 时结果才 为 1。code
(10010011) & (00111101) = (00010001)
C 也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果:
val &= 0377 val = val & 0377
1.1.3 位或(OR): |
二进制运算符 | 经过对两个操做数逐位进行比较产生一个新值。对于每一个位,若是其中任意操做数中对应的位为 1,那么结果位就为 1。
(10010011)| (00111101) = (10111111)
C 也有组合位或-赋值运算符: |=
val |= 0377 val = val | 0377
**1.1.4 位异或: **
二进制运算符^对两个操做数逐位进行比较。对于每一个位,若是操做数中的对应位有一个是 1(但不是都是1),那么结果是 1.若是都是 0 或者都是 1,则结果位 0。
(10010011)^ (00111101) = (10101110)
C 也有一个组合的位异或 - 赋值运算符: ^=
val ^= 0377 val = val ^ 0377
1.1.5 用法
1.1.5.1 打开位
已知:10011010:
1.将位 2 打开
flag | 10011010
(10011010)|(00000100)=(10011110)
2.将全部位打开
flag | ~flag
(10011010)|(01100101)=(11111111)
1.1.5.2 关闭位
flag & ~flag
(10011010)&(01100101)=(00000000)
1.1.5.3 转置位
转置(toggling)一个位表示若是该位打开,则关闭该位;若是该位关闭,则打开。您可使用位异或运算符来转置。其思想是若是 b 是一个位(1或0),那么若是 b 为 1 则 b^1 为 0,若是 b 为 0,则 1^b 为 1。不管 b 的值是 0 仍是 1,0^b 为 b。
flag ^ 0xff
(10010011)^(11111111)=(01101100)
1.1.5.4 交换两个数不须要临时变量
//a ^ b = temp; //a ^ temp = b; //b ^ temp = a (10010011)^(00100110)=(10110101) (10110101)^(00100110)= 10010011 int a = 10; int b = 30;
1.2 移位运算符
如今让咱们了解一下 C 的移位运算符。移位运算符将位向左或向右移动。一样,咱们仍将明确地使用二进制形式来讲明该机制的工做原理。
1.2.1 左移 <<
左移运算符<<将其左侧操做数的值的每位向左移动,移动的位数由其右侧操做数指定。空出来的位用 0 填充,而且丢弃移出左侧操做数末端的位。在下面例子中,每位向左移动两个位置。
(10001010) << 2 = (00101000)
该操做将产生一个新位置,可是不改变其操做数。
1 << 1 = 2; 2 << 1 = 4; 4 << 1 = 8; 8 << 2 = 32
左移一位至关于原值 *2。
1.2.2 右移 >>
右移运算符>>将其左侧的操做数的值每位向右移动,移动的位数由其右侧的操做数指定。丢弃移出左侧操做数有段的位。对于unsigned
类型,使用 0 填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用 0 填充,或者使用符号(最左端)位的副本填充。
//有符号值 (10001010) >> 2 (00100010) //在某些系统上的结果值 (10001010) >> 2 (11100010) //在另外一些系统上的结果 //无符号值 (10001010) >> 2 (00100010) //全部系统上的结果值
1.2.3 用法:移位运算符
移位运算符可以提供快捷、高效(依赖于硬件)对 2 的幂的乘法和除法。
number << n: number乘以2的n次幂
number >> n: 若是number非负,则用number除以2的n次幂
二、数组
2.1 一维数组
-
元素类型角度:数组是相同类型的变量的有序集合
-
内存角度:连续的一大片内存空间
在讨论多维数组以前,咱们还须要学习不少关于一维数组的知识。首先让咱们学习一个概念。
2.1.1 数组名
考虑下面这些声明:
int a; int b[10];
咱们把 a 称做标量,由于它是个单一的值,这个变量是的类型是一个整数。咱们把 b 称做数组,由于它是一些值的集合。下标和数名一块儿使用,用于标识该集合中某个特定的值。例如,b[0] 表示数组 b 的第 1 个值,b[4] 表示第 5 个值。每一个值都是一个特定的标量。
那么问题是 b 的类型是什么?它所表示的又是什么?一个合乎逻辑的答案是它表示整个数组,但事实并不是如此。在 C中,在几乎全部数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:若是他们是int类型,那么数组名的类型就是“指向 int 的常量指针”;若是它们是其余类型,那么数组名的类型也就是“指向其余类型的常量指针”。
请问:指针和数组是等价的吗?
答案是否认的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么状况下不能做为指针常量呢?在如下两种场景下:
-
当数组名做为sizeof操做符的操做数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。
-
当数组名做为&操做符的操做数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。
int arr[10]; //arr = NULL; //arr做为指针常量,不可修改 int *p = arr; //此时arr做为指针常量来使用 printf("sizeof(arr):%d\n", sizeof(arr)); //此时sizeof结果为整个数组的长度 printf("&arr type is %s\n", typeid(&arr).name()); //int(*)[10]而不是int*
2.1.2 下标引用
int arr[] = { 1, 2, 3, 4, 5, 6 };
*(arr + 3)
,这个表达式是什么意思呢?
首先,咱们说数组在表达式中是一个指向整型的指针,因此此表达式表示 arr 指针向后移动了 3 个元素的长度。而后经过间接访问操做符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程彻底相同。因此以下表达式是等同的:
*(arr + 3) arr[3]
问题1:数组下标能否为负值?
问题2:请阅读以下代码,说出结果:
int arr[] = { 5, 3, 6, 8, 2, 9 }; int *p = arr + 2; printf("*p = %d\n", *p); printf("*p = %d\n", p[-1]);
那么是用下标仍是指针来操做数组呢?对于大部分人而言,下标的可读性会强一些。
2.1.3 数组和指针
指针和数组并非相等的。为了说明这个概念,请考虑下面两个声明:
int a[10]; int *b;
声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,而后再建立数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针自己分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。
所以,表达式 *a 是彻底合法的,可是表达式 *b 倒是非法的。*b 将访问内存中一个不肯定的位置,将会致使程序终止。另外一方面b++能够经过编译,a++ 却不行,由于a是一个常量值。
2.1.4 做为函数参数的数组名
当一个数组名做为一个参数传递给一个函数的时候发生什么状况呢?
咱们如今知道数组名其实就是一个指向数组第 1 个元素的指针,因此很明白此时传递给函数的是一份指针的拷贝。因此函数的形参其实是一个指针。可是为了使程序员新手容易上手一些,编译器也接受数组形式的函数形参。所以下面两种函数原型是相等的:
int print_array(int *arr); int print_array(int arr[]);
咱们可使用任何一种声明,但哪个更准确一些呢?答案是指针。由于实参其实是个指针,而不是数组。一样 sizeof arr 值是指针的长度,而不是数组的长度。
如今咱们清楚了,为何一维数组中无须写明它的元素数目了,由于形参只是一个指针,并不须要为数组参数分配内存。另外一方面,这种方式使得函数没法知道数组的长度。若是函数须要知道数组的长度,它必须显式传递一个长度参数给函数。
2.2 多维数组
若是某个数组的维数不止1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。
void test01(){ //二维数组初始化 int arr1[3][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; int arr2[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int arr3[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; //打印二维数组 for (int i = 0; i < 3; i++){ for (int j = 0; j < 3; j ++){ printf("%d ",arr1[i][j]); } printf("\n"); } }
2.2.1 数组名
一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第 1 个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如:
int arr[3][10]
能够理解为这是一个一维数组,包含了 3 个元素,只是每一个元素刚好是包含了 10 个元素的数组。arr 就表示指向它的第1个元素的指针,因此 arr 是一个指向了包含了 10 个整型元素的数组的指针。
2.2.2 指向数组的指针(数组指针)
数组指针,它是指针,指向数组的指针。
数组的类型由元素类型和数组大小共同决定: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"); }
2.2.3 指针数组(元素为指针)
2.2.3.1 栈区指针数组
//数组作函数函数,退化为指针 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); }
2.2.3.2 堆区指针数组
//分配内存 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); }
2.2.4二维数组三种参数形式
2.2.4.1 二维数组的线性存储特性
void PrintArray(int* arr, int len){ for (int i = 0; i < len; i++){ printf("%d ", arr[i]); } printf("\n"); } //二维数组的线性存储 void test(){ int arr[][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; int arr2[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int len = sizeof(arr2) / sizeof(int); //如何证实二维数组是线性的? //经过将数组首地址指针转成Int*类型,那么步长就变成了4,就能够遍历整个数组 int* p = (int*)arr; for (int i = 0; i < len; i++){ printf("%d ", p[i]); } printf("\n"); PrintArray((int*)arr, len); PrintArray((int*)arr2, len); }
2.2.4.2 二维数组的3种形式参数
//二维数组的第一种形式 void PrintArray01(int arr[3][3]){ for (int i = 0; i < 3; i++){ for (int j = 0; j < 3; j++){ printf("arr[%d][%d]:%d\n", i, j, arr[i][j]); } } } //二维数组的第二种形式 void PrintArray02(int arr[][3]){ for (int i = 0; i < 3; i++){ for (int j = 0; j < 3; j++){ printf("arr[%d][%d]:%d\n", i, j, arr[i][j]); } } } //二维数组的第二种形式 void PrintArray03(int(*arr)[3]){ for (int i = 0; i < 3; i++){ for (int j = 0; j < 3; j++){ printf("arr[%d][%d]:%d\n", i, j, arr[i][j]); } } } void test(){ int arr[][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; PrintArray01(arr); PrintArray02(arr); PrintArray03(arr); }
2.3总结
2.3.1 编程提示
-
源代码的可读性几乎老是比程序的运行时效率更为重要
-
只要有可能,函数的指针形参都应该声明为 const。
-
在多维数组的初始值列表中使用完整的多层花括号提升可读性
2.3.2 内容总结
在绝大多数表达式中,数组名的值是指向数组第 1 个元素的指针。这个规则只有两个例外,sizeof 和对数组名&。
指针和数组并不相等。当咱们声明一个数组的时候,同时也分配了内存。可是声明指针的时候,只分配容纳指针自己的空间。
当数组名做为函数参数时,实际传递给函数的是一个指向数组第 1 个元素的指针。
咱们不单能够建立指向普通变量的指针,也可建立指向数组的指针。