《C++杂谈》探讨了指针与常量、迭代器与失效、内联函数、数组名及常量指针等核心概念,剖析了其用法和实现原理。

指针数组和数组指针?变量类型区分

要区分某个变量是指针数组还是数组指针,只需要看 * 修饰的是哪个变量!

  1. int main() {
  2. int a[3] = {1, 2, 3}; // 数组
  3. int* b[3]; // 指针数组
  4. int(*c)[3]; // 数组指针
  5. int(*e[3]); // 指针数组,跟 b 一样
  6. void (*funcName)(int a); // 函数指针
  7. funcName = getName;
  8. funcName(1);
  9. void (*funcNameArr[3])(int a); // 函数指针的一维数组
  10. void (**funcNameArr1[3])(int a); // 函数指针的二维数组
  11. int randomNum = 1997;
  12. int randomNum2 = 1998;
  13. const int* constp1 = &randomNum; // 指针常量
  14. constp1 = &randomNum2; // 编译通过
  15. //*constp1 = 1; // 编译失败
  16. int* const constp2 = &randomNum; // 常量指针
  17. *constp2 = 2; // 编译通过
  18. //constp2 = &randomNum2; // 编译失败
  19. cout << "end!" << endl;
  20. }

指针常量与常量指针

指针常量与常量指针经常容易混淆。通过实际代码,我们可以更好地理解它们的区别。

方法1: 常量指针

  1. const double* ptr; // const 读作常量,* 读作指针,按照顺序读作常量指针

方法2: 常量指针

  1. double const* ptr; // const 读作常量,* 读作指针,按照顺序读作常量指针

指针常量

  1. double* const ptr; // const 读作常量,* 读作指针,按照顺序读作指针常量

常量的意思

在 C/C++ 中,常量的关键词是 const,即无法被改变。在编译阶段,编译器若发现对常量进行了修改,就会出现提示。基于此,常量在声明时就必须初始化,而且之后都不能改变。

若不初始化:

  1. const int x; // 编译失败,必须初始化

若尝试改变:

  1. const int x = 10;
  2. x = 20; // 编译失败

指针的概念

简单来说,指针就是一个盒子,里边放着的东西是一把钥匙,我们可以通过这把钥匙去打开一个对应的保险箱并取出东西。

  • 盒子 = 指针:根据系统位数32/64位数不同,这个盒子的大小可能为4/8字节大小。
  • 钥匙 = 内存地址:根据系统位数32/64不同,这个钥匙大小也是4/8字节。
  • 保险箱 = 内存空间:利用钥匙中隐藏的内存地址,访问对应内存地址的内存空间,取出其中的宝藏!

常量指针与指针常量

指针常量

指针本身是常量,指针内部存的钥匙是无法改变的,但指针指向的内容是可以改变的。

  1. int randomNum = 1997;
  2. int* const constp2 = &randomNum; // 常量指针
  3. *constp2 = 2; // 编译通过
  4. //constp2 = &randomNum2; // 编译失败

常量指针

指针指向常量,指针本身可以改变,但指针指向的内容不能改变。

  1. int randomNum = 1997;
  2. const int* constp1 = &randomNum; // 指向常量的指针
  3. // *constp1 = 1; // 编译失败
  4. int randomNum2 = 1998;
  5. constp1 = &randomNum2; // 编译通过

示例代码

  1. int main() {
  2. int randomNum = 1997;
  3. int randomNum2 = 1998;
  4. // 指向常量的指针
  5. const int* constp1 = &randomNum;
  6. constp1 = &randomNum2; // 编译通过
  7. //*constp1 = 1; // 编译失败
  8. // 常量指针
  9. int* const constp2 = &randomNum;
  10. *constp2 = 2; // 编译通过
  11. //constp2 = &randomNum2; // 编译失败
  12. cout << "end!" << endl;
  13. }

总结

  • 指针常量:指针本身不能改变,但指针指向的内容可以改变。
  • 常量指针:指针本身可以改变,但指针指向的内容不能改变。

字符串和字符串数组

  • 字符串:存储在代码段中的字符串数组,无法修改。
  • 字符串数组:存储在栈区的数组,可以修改。

全局变量、全局函数同名

  • 全局变量和全局函数:不能同名(函数可以同名,因为重载)。
  • 全局函数和局部变量:可以同名。

迭代器与失效

迭代器的作用

STL标准库一共有六大部件:分配器、容器、迭代器、算法、仿函数、适配器。迭代器是用来“联结”算法、仿函数与容器的纽带。

迭代器模式

迭代器模式是一种设计模式,提供了一种方法,在不需要暴露某个容器的内部表现形式情况下,使之能依次访问该容器中的各个元素。这种设计思维在STL中得到了广泛的应用,是STL的关键所在。

迭代器的实现

vector

在vector中,迭代器被定义成了我们传入的类型参数T的指针。

  1. template<typename T, class Alloc = alloc>
  2. class vector {
  3. public:
  4. typedef T value_type;
  5. typedef value_type* iterator;
  6. // 其他代码省略
  7. };

List

以下是一个简单的 List 迭代器实现:

  1. #ifndef CPP_PRIMER_MY_LIST_H
  2. #define CPP_PRIMER_MY_LIST_H
  3. #include <iostream>
  4. template<typename T>
  5. class node {
  6. public:
  7. T value;
  8. node* next;
  9. node() : next(nullptr) {}
  10. node(T val, node* p = nullptr) : value(val), next(p) {}
  11. };
  12. template<typename T>
  13. class my_list {
  14. private:
  15. node<T>* head;
  16. node<T>* tail;
  17. int size;
  18. private:
  19. class list_iterator {
  20. private:
  21. node<T>* ptr; // 指向 list 容器中的某个元素的指针
  22. public:
  23. list_iterator(node<T>* p = nullptr) : ptr(p) {}
  24. // 重载 ++、--、*、-> 等基本操作
  25. // 返回引用,方便通过 *it 来修改对象
  26. T& operator*() const {
  27. return ptr->value;
  28. }
  29. node<T>* operator->() const {
  30. return ptr;
  31. }
  32. list_iterator& operator++() {
  33. ptr = ptr->next;
  34. return *this;
  35. }
  36. list_iterator operator++(int) {
  37. node<T>* tmp = ptr;
  38. // this 是指向 list_iterator 的常量指针,因此 *this 就是 list_iterator 对象,前置++已经被重载过
  39. ++(*this);
  40. return list_iterator(tmp);
  41. }
  42. bool operator==(const list_iterator& t) const {
  43. return t.ptr == this->ptr;
  44. }
  45. bool operator!=(const list_iterator& t) const {
  46. return t.ptr != this->ptr;
  47. }
  48. };
  49. public:
  50. typedef list_iterator iterator; // 类型别名
  51. my_list() {
  52. head = nullptr;
  53. tail = nullptr;
  54. size = 0;
  55. }
  56. // 从链表尾部插入元素
  57. void push_back(const T& value) {
  58. if (head == nullptr) {
  59. head = new node<T>(value);
  60. tail = head;
  61. } else {
  62. tail->next = new node<T>(value);
  63. tail = tail->next;
  64. }
  65. size++;
  66. }
  67. // 打印链表元素
  68. void print(std::ostream& os = std::cout) const {
  69. for (node<T>* ptr = head; ptr != tail->next; ptr = ptr->next) {
  70. os << ptr->value << std::endl;
  71. }
  72. }
  73. public:
  74. // 操作迭代器的方法
  75. // 返回链表头部指针
  76. iterator begin() const {
  77. return list_iterator(head);
  78. }
  79. // 返回链表尾部指针
  80. iterator end() const {
  81. return list_iterator(tail->next);
  82. }
  83. // 其他成员函数 insert/erase/emplace
  84. };
  85. #endif // CPP_PRIMER_MY_LIST_H

其他容器迭代器分析

vector: 迭代器具有所有指针算术运算能力,种类为 Random Access Iterator。

list: 由于是双向链表,迭代器只有双向读写,但不能随机访问元素,种类为 Bidirectional Iterator。

迭代器分类

在 STL 中,除了原生指针以外,迭代器被分为五类:

  1. Input Iterator:只读迭代器,支持 ==、!=、++、*、-> 操作。
  2. Output Iterator:只写迭代器,支持 ++、* 操作。
  3. Forward Iterator:单向读写迭代器,支持 Input Iterator 和 Output Iterator 的所有操作。
  4. Bidirectional Iterator:双向读写迭代器,支持 Forward Iterator 的所有操作,并支持 — 操作。
  5. Random Access Iterator:支持所有指针操作,适用于随机访问,支持前四种迭代器的所有操作,并支持 it + n、it - n、it += n、it -= n、it1 - it2 和 it[n] 操作。

迭代器失效

当使用一个容器的 insert 或 erase 函数通过迭代器插入、删除或者修改元素时,可能会导致迭代器失效。

vector 迭代器失效情况

  1. 插入(push_back)一个元素后,end 操作返回的迭代器肯定失效。
  2. 当插入(push_back)一个元素后,如果 capacity 返回值与没有插入元素之前相比有变化,则 first 和 end 操作返回的迭代器都会失效。
  3. 删除操作(erase,pop_back)后,指向删除点的迭代器和删除点后面的元素的迭代器都会失效。

list 迭代器失效情况

  1. 插入操作(insert)和接合操作(splice)不会导致原有的 list 迭代器失效。
  2. 删除操作(erase)仅仅会使指向被删除元素的迭代器失效,其他迭代器不受影响。

关联容器迭代器失效情况

对于关联容器(如 map, set, multimap, multiset),删除当前的迭代器仅会使当前迭代器失效。为了避免危险,应该重新获取新的有效的迭代器。

a++和a++的代码实现

a++ 和 ++a 的区别在于,前者是值,后者是引用。

示例代码

  1. #include <iostream>
  2. using namespace std;
  3. int main() {
  4. int a = 10;
  5. printf("%d\n", a++);
  6. printf("%d\n", ++a);
  7. a = a++;
  8. printf("%d\n", a);
  9. return 0;
  10. }

结果分析

  1. 10
  2. 12
  3. 12

实现

后缀实现

  1. T T::operator++(int) {
  2. T old(*this);
  3. *this = *this + 1;
  4. return old;
  5. }

前缀实现

  1. T& T::operator++() {
  2. *this = *this + 1;
  3. return *this;
  4. }

C语言与汇编

编译过程

  1. 预处理:完成文本替换、宏展开以及删除注释,生成 .i 文件。
  2. 编译:将 .i 文件翻译为 .s,得到汇编程序语言。
  3. 汇编:将 .s 文件翻译成机器语言指令,生成目标文件 .o
  4. 链接:将目标文件与库文件链接,生成可执行文件。

查看汇编代码

使用 gcc/g++ 查看各阶段代码:

  1. gcc -S test.c -o test.s

示例代码

有函数调用版本

  1. #include <stdio.h>
  2. int add(int a, int b) {
  3. return a + b;
  4. }
  5. int main() {
  6. int a = 1;
  7. int b = 2;
  8. int c;
  9. c = add(a, b);
  10. return 0;
  11. }

汇编代码

  1. .file "test_inline_call.c"
  2. .text
  3. .globl add
  4. .type add, @function
  5. add:
  6. .LFB0:
  7. .cfi_startproc
  8. pushq %rbp
  9. .cfi_def_cfa_offset 16
  10. .cfi_offset 6, -16
  11. movq %rsp, %rbp
  12. .cfi_def_cfa_register 6
  13. movl %edi, -4(%rbp)
  14. movl %esi, -8(%rbp)
  15. movl -4(%rbp), %edx
  16. movl -8(%rbp), %eax
  17. addl %edx, %eax
  18. popq %rbp
  19. .cfi_def_cfa 7, 8
  20. ret
  21. .cfi_endproc
  22. .LFE0:
  23. .size add, .-add
  24. .globl main
  25. .type main, @function
  26. main:
  27. .LFB1:
  28. .cfi_startproc
  29. pushq %rbp
  30. .cfi_def_cfa_offset 16
  31. .cfi_offset 6, -16
  32. movq %rsp, %rbp
  33. .cfi_def_cfa_register 6
  34. subq $16, %rsp
  35. movl $1, -12(%rbp)
  36. movl $2, -8(%rbp)
  37. movl -8(%rbp), %edx
  38. movl -12(%rbp), %eax
  39. movl %edx, %esi
  40. movl %eax, %edi
  41. call add
  42. movl %eax, -4(%rbp)
  43. movl $0, %eax
  44. leave
  45. .cfi_def_cfa 7, 8
  46. ret
  47. .cfi_endproc
  48. .LFE1:
  49. .size main, .-main
  50. .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
  51. .section .note.GNU-stack,"",@progbits

无函数调用版本

  1. #include <stdio.h>
  2. int main() {
  3. int a = 1;
  4. int b = 2;
  5. int c;
  6. c = a + b;
  7. return 0;
  8. }

汇编代码

  1. .file "test_inline_nocall.c"
  2. .text
  3. .globl main
  4. .type main, @function
  5. main:
  6. .LFB0:
  7. .cfi_startproc
  8. pushq %rbp
  9. .cfi_def_cfa_offset 16
  10. .cfi_offset 6, -16
  11. movq %rsp, %rbp
  12. .cfi_def_cfa_register 6
  13. movl $1, -12(%rbp)
  14. movl $2, -8(%rbp)
  15. movl -12(%rbp), %edx
  16. movl -8(%rbp), %eax
  17. addl %edx, %eax
  18. movl %eax, -4(%rbp)
  19. movl $0, %eax
  20. popq %rbp
  21. .cfi_def_cfa 7, 8
  22. ret
  23. .cfi_endproc
  24. .LFE0:
  25. .size main, .-main
  26. .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
  27. .section .note.GNU-stack,"",@progbits

对比

  • 带函数调用的版本在多了参数入栈、控制权转移、栈帧恢复等汇编代码。
  • 使用内联函数可以减少这些开销。

内联函数版本

  1. #include <stdio.h>
  2. __attribute__((always_inline))
  3. inline int add(int a, int b) {
  4. return a + b;
  5. }
  6. int main() {
  7. int a = 1;
  8. int b = 2;
  9. int c;
  10. c = add(a, b);
  11. return 0;
  12. }

汇编代码

  1. .file "test_inline.c"
  2. .text
  3. .globl main
  4. .type main, @function
  5. main:
  6. .LFB1:
  7. .cfi_startproc
  8. pushq %rbp
  9. .cfi_def_cfa_offset 16
  10. .cfi_offset 6, -16
  11. movq %rsp, %rbp
  12. .cfi_def_cfa_register 6
  13. movl $1, -20(%rbp)
  14. movl $2, -16(%rbp)
  15. movl -20(%rbp), %eax
  16. movl %eax, -8(%rbp)
  17. movl -16(%rbp), %eax
  18. movl %eax, -4(%rbp)
  19. movl -8(%rbp), %edx
  20. movl -4(%rbp), %eax
  21. addl %edx, %eax
  22. movl %eax, -12(%rbp)
  23. movl $0, %eax
  24. popq %rbp
  25. .cfi_def_cfa 7, 8
  26. ret
  27. .cfi_endproc
  28. .LFE1:
  29. .size main, .-main
  30. .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609"
  31. .section .note.GNU-stack,"",@progbits

对比分析

  • 函数调用版本多了栈帧更新、控制权转移等操作。
  • 内联函数版本避免了这些操作,将函数代码直接嵌入调用处。

小结

内联函数通过嵌入代码,减少了函数调用的开销。虽然会增加代码膨胀,但对于小型、频繁调用的函数,可以显著提升性能。

数组名与常量指针

示例代码

  1. char* s = "abc";
  2. char a[4];
  3. s = a; // 合法
  4. a = s; // 非法,数组名是常量指针

数组名不是常量指针的情况

  1. 数组名作为 sizeof 操作符的操作数时,表示整个数组。
  2. 数组名作为 & 操作符的操作数时,表示整个数组。

示例

sizeof 操作符

  1. int arr[5] = {1, 2, 3, 4, 5};
  2. int arrSize = sizeof(arr); // arr 表示整个数组,arrSize = 20

& 操作符

  1. int arr[5] = {1, 2, 3, 4, 5};
  2. int* p = &arr; // arr 表示整个数组,p 的值为数组首元素的地址

指针数组与数组指针

示例代码

  1. char* p[4]; // 指针数组,p 指向的是一个数组,里面存的是4个指针
  2. char (*p)[4]; // 数组指针,p 是指向一个数组首地址,这个数组里的每个元素都是 char

总结

  • 指针数组:数组中的每个元素都是指针。
  • 数组指针:指向一个数组的指针。