10W+字C语言硬核总结(三),值得阅读收藏!

举报
C语言与CPP编程 发表于 2021/07/28 00:52:09 2021/07/28
【摘要】 1、位运算 可以使用 C 对变量中的个别位进行操作。您可能对人们想这样做的原因感到奇怪。这种能力有时确实是必须的,或者至少是有用的。C 提供位的逻辑运算符和移位运算符。在以下例子中,我们将使用二进制计数法写出值,以便您可以了解对位发生的操作。在一个实际程序中,您可以使用一般的形式的整数变量或常量。例如不适用 00011001 的形式,而写为 25 或者 031 或者 0x1...

1、位运算

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

10W+字C语言硬核总结(一),值得阅读收藏!

10W+字C语言硬核总结(二),值得阅读收藏!

程序员必备硬核资源,点击下载!

1.1 位逻辑运算符

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

1.1.1 按位取反~

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


  
  1. ~(10011010)
  2. 01100101

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


  
  1. unsigned char a = 2;   //00000010
  2. unsigned char b = ~a;  //11111101
  3. printf("ret = %d\n", a); //ret = 2
  4. printf("ret = %d\n", b); //ret = 253

1.1.2 位与(AND): &

二进制运算符 & 通过对两个操作数逐位进行比较产生一个新值。对于每个位,只有两个操作数的对应位都是 1 时结果才 为 1。

(10010011) & (00111101) = (00010001)

C 也有一个组合的位与-赋值运算符:&=。下面两个将产生相同的结果:


  
  1. val &= 0377
  2. val = val & 0377

1.1.3 位或(OR): |

二进制运算符 | 通过对两个操作数逐位进行比较产生一个新值。对于每个位,如果其中任意操作数中对应的位为 1,那么结果位就为 1。

(10010011)| (00111101) = (10111111)

C 也有组合位或-赋值运算符: |=


  
  1. val |= 0377
  2. val = val | 0377

**1.1.4 位异或: **

二进制运算符^对两个操作数逐位进行比较。对于每个位,如果操作数中的对应位有一个是 1(但不是都是1),那么结果是 1.如果都是 0 或者都是 1,则结果位 0。

(10010011)^ (00111101) = (10101110)

C 也有一个组合的位异或 - 赋值运算符: ^=


  
  1. val ^= 0377
  2. 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 交换两个数不需要临时变量


  
  1. //a ^ b = temp;
  2. //a ^ temp = b;
  3. //b ^ temp = a
  4.  (10010011)^(00100110)=(10110101)
  5.  (10110101)^(00100110)= 10010011
  6.  
  7.   int a = 10;
  8.   int b = 30;

1.2 移位运算符

现在让我们了解一下 C 的移位运算符。移位运算符将位向左或向右移动。同样,我们仍将明确地使用二进制形式来说明该机制的工作原理。

1.2.1 左移 <<

左移运算符<<将其左侧操作数的值的每位向左移动,移动的位数由其右侧操作数指定。空出来的位用 0 填充,并且丢弃移出左侧操作数末端的位。在下面例子中,每位向左移动两个位置。

(10001010) << 2 = (00101000)

该操作将产生一个新位置,但是不改变其操作数。


  
  1. 1 << 1 = 2;
  2. 2 << 1 = 4;
  3. 4 << 1 = 8;
  4. 8 << 2 = 32

左移一位相当于原值 *2。

1.2.2 右移 >>

右移运算符>>将其左侧的操作数的值每位向右移动,移动的位数由其右侧的操作数指定。丢弃移出左侧操作数有段的位。对于unsigned类型,使用 0 填充左端空出的位。对于有符号类型,结果依赖于机器。空出的位可能用 0 填充,或者使用符号(最左端)位的副本填充。


  
  1. //有符号值
  2. (10001010) >> 2
  3. (00100010)     //在某些系统上的结果值
  4. (10001010) >> 2
  5. (11100010)     //在另一些系统上的结果
  6. //无符号值
  7. (10001010) >> 2
  8. (00100010)    //所有系统上的结果值

1.2.3 用法:移位运算符

移位运算符能够提供快捷、高效(依赖于硬件)对 2 的幂的乘法和除法。

number << n: number乘以2的n次幂

number >> n: 如果number非负,则用number除以2的n次幂

程序员必备硬核资源,点击下载!

2、数组

2.1 一维数组

  • 元素类型角度:数组是相同类型的变量的有序集合

  • 内存角度:连续的一大片内存空间

在讨论多维数组之前,我们还需要学习很多关于一维数组的知识。首先让我们学习一个概念。

2.1.1 数组名

考虑下面这些声明:


  
  1. int a;
  2. int b[10];

我们把 a 称作标量,因为它是个单一的值,这个变量是的类型是一个整数。我们把 b 称作数组,因为它是一些值的集合。下标和数名一起使用,用于标识该集合中某个特定的值。例如,b[0] 表示数组 b 的第 1 个值,b[4] 表示第 5 个值。每个值都是一个特定的标量。

那么问题是 b 的类型是什么?它所表示的又是什么?一个合乎逻辑的答案是它表示整个数组,但事实并非如此。在 C中,在几乎所有数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。它的类型取决于数组元素的类型:如果他们是int类型,那么数组名的类型就是“指向 int 的常量指针”;如果它们是其他类型,那么数组名的类型也就是“指向其他类型的常量指针”。

请问:指针和数组是等价的吗?

答案是否定的。数组名在表达式中使用的时候,编译器才会产生一个指针常量。那么数组在什么情况下不能作为指针常量呢?在以下两种场景下:

  • 当数组名作为sizeof操作符的操作数的时候,此时sizeof返回的是整个数组的长度,而不是指针数组指针的长度。

  • 当数组名作为&操作符的操作数的时候,此时返回的是一个指向数组的指针,而不是指向某个数组元素的指针常量。


  
  1. int arr[10];
  2. //arr = NULL; //arr作为指针常量,不可修改
  3. int *p = arr; //此时arr作为指针常量来使用
  4. printf("sizeof(arr):%d\n"sizeof(arr)); //此时sizeof结果为整个数组的长度
  5. printf("&arr type is %s\n"typeid(&arr).name()); //int(*)[10]而不是int*

2.1.2 下标引用

int arr[] = { 123456 };

 

*(arr + 3) ,这个表达式是什么意思呢?

首先,我们说数组在表达式中是一个指向整型的指针,所以此表达式表示 arr 指针向后移动了 3 个元素的长度。然后通过间接访问操作符从这个新地址开始获取这个位置的值。这个和下标的引用的执行过程完全相同。所以如下表达式是等同的:


  
  1. *(arr + 3)
  2. arr[3]

问题1:数组下标可否为负值?

问题2:请阅读如下代码,说出结果:


  
  1. int arr[] = { 536829 };
  2. int *p = arr + 2;
  3. printf("*p = %d\n", *p);
  4. printf("*p = %d\n", p[-1]);

那么是用下标还是指针来操作数组呢?对于大部分人而言,下标的可读性会强一些。

2.1.3 数组和指针

指针和数组并不是相等的。为了说明这个概念,请考虑下面两个声明:


  
  1. int a[10];
  2. int *b;

声明一个数组时,编译器根据声明所指定的元素数量为数组分配内存空间,然后再创建数组名,指向这段空间的起始位置。声明一个指针变量的时候,编译器只为指针本身分配内存空间,并不为任何整型值分配内存空间,指针并未初始化指向任何现有的内存空间。

因此,表达式 *a 是完全合法的,但是表达式 *b 却是非法的。*b 将访问内存中一个不确定的位置,将会导致程序终止。另一方面b++可以通过编译,a++ 却不行,因为a是一个常量值。

2.1.4 作为函数参数的数组名

当一个数组名作为一个参数传递给一个函数的时候发生什么情况呢?

我们现在知道数组名其实就是一个指向数组第 1 个元素的指针,所以很明白此时传递给函数的是一份指针的拷贝。所以函数的形参实际上是一个指针。但是为了使程序员新手容易上手一些,编译器也接受数组形式的函数形参。因此下面两种函数原型是相等的:


  
  1. int print_array(int *arr);
  2. int print_array(int arr[]);

我们可以使用任何一种声明,但哪一个更准确一些呢?答案是指针。因为实参实际上是个指针,而不是数组。同样 sizeof arr 值是指针的长度,而不是数组的长度。

现在我们清楚了,为什么一维数组中无须写明它的元素数目了,因为形参只是一个指针,并不需要为数组参数分配内存。另一方面,这种方式使得函数无法知道数组的长度。如果函数需要知道数组的长度,它必须显式传递一个长度参数给函数。

2.2 多维数组

如果某个数组的维数不止1个,它就被称为多维数组。接下来的案例讲解以二维数组举例。


  
  1. void test01(){
  2.  //二维数组初始化
  3.  int arr1[3][3] = {
  4.   { 123 },
  5.   { 456 },
  6.   { 789 }
  7.  };
  8.  int arr2[3][3] = { 123456789 };
  9.  int arr3[][3] = { 123456789 };
  10.  //打印二维数组
  11.  for (int i = 0; i < 3; i++){
  12.   for (int j = 0; j < 3; j ++){
  13.    printf("%d ",arr1[i][j]);
  14.   }
  15.   printf("\n");
  16.  }
  17. }

2.2.1 数组名

一维数组名的值是一个指针常量,它的类型是“指向元素类型的指针”,它指向数组的第 1 个元素。多维数组也是同理,多维数组的数组名也是指向第一个元素,只不过第一个元素是一个数组。例如:

int arr[3][10]

 

可以理解为这是一个一维数组,包含了 3 个元素,只是每个元素恰好是包含了 10 个元素的数组。arr 就表示指向它的第1个元素的指针,所以 arr 是一个指向了包含了 10 个整型元素的数组的指针。

2.2.2 指向数组的指针(数组指针)

数组指针,它是指针,指向数组的指针。

数组的类型由元素类型和数组大小共同决定:int array[5] 的类型为 int[5]

C 语言可通过 typedef 定义一个数组类型:

定义数组指针有一下三种方式:


  
  1. //方式一
  2. void test01(){
  3.  //先定义数组类型,再用数组类型定义数组指针
  4.  int arr[10] = {1,2,3,4,5,6,7,8,9,10};
  5.  //有typedef是定义类型,没有则是定义变量,下面代码定义了一个数组类型ArrayType
  6.  typedef int(ArrayType)[10];
  7.  //int ArrayType[10]; //定义一个数组,数组名为ArrayType
  8.  ArrayType myarr; //等价于 int myarr[10];
  9.  ArrayType* pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
  10.  for (int i = 0; i < 10;i++){
  11.   printf("%d ",(*pArr)[i]);
  12.  }
  13.  printf("\n");
  14. }
  15. //方式二
  16. void test02(){
  17.  int arr[10];
  18.  //定义数组指针类型
  19.  typedef int(*ArrayType)[10];
  20.  ArrayType pArr = &arr; //定义了一个数组指针pArr,并且指针指向数组arr
  21.  for (int i = 0; i < 10; i++){
  22.   (*pArr)[i] = i + 1;
  23.  }
  24.  for (int i = 0; i < 10; i++){
  25.   printf("%d ", (*pArr)[i]);
  26.  }
  27.  printf("\n");
  28. }
  29. //方式三
  30. void test03(){
  31.  
  32.  int arr[10];
  33.  int(*pArr)[10] = &arr;
  34.  for (int i = 0; i < 10; i++){
  35.   (*pArr)[i] = i + 1;
  36.  }
  37.  for (int i = 0; i < 10; i++){
  38.   printf("%d ", (*pArr)[i]);
  39.  }
  40.  printf("\n");
  41. }

2.2.3 指针数组(元素为指针)

2.2.3.1 栈区指针数组


  
  1. //数组做函数函数,退化为指针
  2. void array_sort(char** arr,int len){
  3.  for (int i = 0; i < len; i++){
  4.   for (int j = len - 1; j > i; j --){
  5.    //比较两个字符串
  6.    if (strcmp(arr[j-1],arr[j]) > 0){
  7.     char* temp = arr[j - 1];
  8.     arr[j - 1] = arr[j];
  9.     arr[j] = temp;
  10.    }
  11.   }
  12.  }
  13. }
  14. //打印数组
  15. void array_print(char** arr,int len){
  16.  for (int i = 0; i < len;i++){
  17.   printf("%s\n",arr[i]);
  18.  }
  19.  printf("----------------------\n");
  20. }
  21. void test(){
  22.  
  23.  //主调函数分配内存
  24.  //指针数组
  25.  char* p[] = { "bbb""aaa""ccc""eee""ddd"};
  26.  //char** p = { "aaa", "bbb", "ccc", "ddd", "eee" }; //错误
  27.  int len = sizeof(p) / sizeof(char*);
  28.  //打印数组
  29.  array_print(p, len);
  30.  //对字符串进行排序
  31.  array_sort(p, len);
  32.  //打印数组
  33.  array_print(p, len);
  34. }

2.2.3.2 堆区指针数组


  
  1. //分配内存
  2. char** allocate_memory(int n){
  3.  
  4.  if (n < 0 ){
  5.   return NULL;
  6.  }
  7.  char** temp = (char**)malloc(sizeof(char*) * n);
  8.  if (temp == NULL){
  9.   return NULL;
  10.  }
  11.  //分别给每一个指针malloc分配内存
  12.  for (int i = 0; i < n; i ++){
  13.   temp[i] = malloc(sizeof(char)* 30);
  14.   sprintf(temp[i], "%2d_hello world!", i + 1);
  15.  }
  16.  return temp;
  17. }
  18. //打印数组
  19. void array_print(char** arr,int len){
  20.  for (int i = 0; i < len;i++){
  21.   printf("%s\n",arr[i]);
  22.  }
  23.  printf("----------------------\n");
  24. }
  25. //释放内存
  26. void free_memory(char** buf,int len){
  27.  if (buf == NULL){
  28.   return;
  29.  }
  30.  for (int i = 0; i < len; i ++){
  31.   free(buf[i]);
  32.   buf[i] = NULL;
  33.  }
  34.  free(buf);
  35. }
  36. void test(){
  37.  
  38.  int n = 10;
  39.  char** p = allocate_memory(n);
  40.  //打印数组
  41.  array_print(p, n);
  42.  //释放内存
  43.  free_memory(p, n);
  44. }

2.2.4二维数组三种参数形式

2.2.4.1 二维数组的线性存储特性


  
  1. void PrintArray(int* arr, int len){
  2.  for (int i = 0; i < len; i++){
  3.   printf("%d ", arr[i]);
  4.  }
  5.  printf("\n");
  6. }
  7. //二维数组的线性存储
  8. void test(){
  9.  int arr[][3] = {
  10.   { 123 },
  11.   { 456 },
  12.   { 789 }
  13.  };
  14.  int arr2[][3] = { 123456789 };
  15.  int len = sizeof(arr2) / sizeof(int);
  16.  //如何证明二维数组是线性的?
  17.  //通过将数组首地址指针转成Int*类型,那么步长就变成了4,就可以遍历整个数组
  18.  int* p = (int*)arr;
  19.  for (int i = 0; i < len; i++){
  20.   printf("%d ", p[i]);
  21.  }
  22.  printf("\n");
  23.  PrintArray((int*)arr, len);
  24.  PrintArray((int*)arr2, len);
  25. }

2.2.4.2 二维数组的3种形式参数


  
  1. //二维数组的第一种形式
  2. void PrintArray01(int arr[3][3]){
  3.  for (int i = 0; i < 3; i++){
  4.   for (int j = 0; j < 3; j++){
  5.    printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
  6.   }
  7.  }
  8. }
  9. //二维数组的第二种形式
  10. void PrintArray02(int arr[][3]){
  11.  for (int i = 0; i < 3; i++){
  12.   for (int j = 0; j < 3; j++){
  13.    printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
  14.   }
  15.  }
  16. }
  17. //二维数组的第二种形式
  18. void PrintArray03(int(*arr)[3]){
  19.  for (int i = 0; i < 3; i++){
  20.   for (int j = 0; j < 3; j++){
  21.    printf("arr[%d][%d]:%d\n", i, j, arr[i][j]);
  22.   }
  23.  }
  24. }
  25. void test(){
  26.  
  27.  int arr[][3] = { 
  28.   { 123 },
  29.   { 456 },
  30.   { 789 }
  31.  };
  32.  
  33.  PrintArray01(arr);
  34.  PrintArray02(arr);
  35.  PrintArray03(arr);
  36. }

2.3总结

2.3.1 编程提示

  • 源代码的可读性几乎总是比程序的运行时效率更为重要

  • 只要有可能,函数的指针形参都应该声明为 const。

  • 在多维数组的初始值列表中使用完整的多层花括号提高可读性

2.3.2 内容总结

在绝大多数表达式中,数组名的值是指向数组第 1 个元素的指针。这个规则只有两个例外,sizeof 和对数组名&。

指针和数组并不相等。当我们声明一个数组的时候,同时也分配了内存。但是声明指针的时候,只分配容纳指针本身的空间。

当数组名作为函数参数时,实际传递给函数的是一个指向数组第 1 个元素的指针。

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

程序员必备硬核资源,点击下载

文章来源: blog.csdn.net,作者:C语言与CPP编程,版权归原作者所有,如需转载,请联系作者。

原文链接:blog.csdn.net/weixin_41055260/article/details/119153223

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。