3

熬夜整理的c/c++万字总结(三),值得收藏!

 3 years ago
source link: https://my.oschina.net/u/4586915/blog/5161778
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1、位运算

可以使用 C 对变量中的个别位进行操作。您可能对人们想这样做的原因感到奇怪。这种能力有时确实是必须的,或者至少是有用的。C 提供位的逻辑运算符和移位运算符。在以下例子中,我们将使用二进制计数法写出值,以便您可以了解对位发生的操作。在一个实际程序中,您可以使用一般的形式的整数变量或常量。例如不适用 00011001 的形式,而写为 25 或者 031 或者 0x19.在我们的例子中,我们将使用8位数字,从左到右,每位的编号是 7 到 0。

这一定是你需要的电子书资源,全!点击查看!

程序员书籍资源,点击查看!

1.1 位逻辑运算符

4 个位运算符用于整型数据,包括 char。将这些位运算符成为位运算的原因是它们对每位进行操作,而不影响左右两侧的位。请不要将这些运算符与常规的逻辑运算符(&& 、||和!)相混淆,常规的位的逻辑运算符对整个值进行操作。

1.1.1 按位取反~

一元运算符~将每个 1 变为 0,将每个 0 变为 1,如下面的例子:

~(10011010)
01100101

假设 a 是一个unsigned char,已赋值为 2。在二进制中,2 是00000010.于是 -a 的值为11111101或者 253。请注意该运算符不会改变 a 的值,a 仍为 2。

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。

(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、数组

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 个元素的指针。

我们不单可以创建指向普通变量的指针,也可创建指向数组的指针。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK