import{_ as s,c as i,o as t,a6 as a}from"./chunks/framework.CZKtKhAm.js";const d="/c/assets/1.L8V3GBrc.png",l="/c/assets/2.CdvhiwcU.png",e="/c/assets/3.D74t3-Xt.png",y=JSON.parse('{"title":"第一章:颇具争议的指针","description":"","frontmatter":{},"headers":[],"relativePath":"notes/01_c-basic/06_xdx/index.md","filePath":"notes/01_c-basic/06_xdx/index.md","lastUpdated":1722496166000}'),n={name:"notes/01_c-basic/06_xdx/index.md"},o=a('
指针
是 C 语言中最重要
的概念之一,也是最难以理解
的概念之一。指针
是 C 语言的精髓
,要想掌握 C 语言就需要深入地了解指针。
强大
和灵活
的工具;但是,需要开发者小心谨慎的使用
,以确保程序的稳定性和安全性。NOTE
之所以指针在 C 语言中颇具争议,是因为一方面其功能强大,直接操作内存地址;另一方面,又很危险,不正确的使用指针的方式,非常容易导致程序崩溃。
如果没有能很好的使用指针,就会带来一系列的问题,如:
为了减少指针带来的风险,开发人员可以采取以下的措施:
IMPORTANT
C++
采用了如下的策略和机制,来解决指针危险操作的:
std::shared_ptr
、std::unique_ptr
),这些指针提供了自动资源管理和所有权的语义。std::unique_ptr
确保只有一个指针可以访问给定的资源,从而避免了传统指针的悬空引用和内存泄漏问题。std::shared_ptr
允许多个指针共享一个资源,并在所有引用释放后自动释放。&
符号)提供了更安全的间接访问方法,与指针相比,引用不能重新绑定到不同的对象,从而减少了意外的指针错误。Go
采用了如下的策略和机制,来解决指针危险操作的:
Rust
采用了如下的策略和机制,来解决指针危险操作的:
Java
采用了如下的策略和机制,来解决指针危险操作的:
IMPORTANT
总而言之,各种编程语言通过引入不同的策略和机制,如:智能指针、垃圾回收器、所有权和借用,以及强类型系统,有效地减少了指针操作所带来的各种安全性和可靠性问题,提升了程序的稳定性和开发效率。
数据类型 变量名 = 值 ;
IMPORTANT
变量名(标识符)需要符合命名规则和命名规范!!!
小写
或大写英文字母
,0-9
或 _
组成。数字
开头。关键字
。长度
限制,不同编译器和平台会有所不同,一般限制在 63 个字符内。区分大小写字母
,如:Hello、hello 是不同的标识符。_Bool
,为防止冲突,建议开发者尽量避免使用下划线开头的标识符。变量名
的作用
,如下所示: 编写
代码的时候,使用变量名
来关联
某块内存的地址
。执行
的时候,会将变量名替换
为具体的地址,再进行具体的操作。变量
中存储
的值
的不同
,我们可以将变量
分为两类: 普通变量
:变量所对应的内存中存储的是普通值
。指针变量
:变量所对应的内存中存储的是另一个变量的地址
。普通变量
所对应的内存空间存储
的是普通的值
,如:整数、小数、字符等;指针变量
所对应的内存空间存储
的是另外一个变量的地址
。普通变量有普通变量的运算方式
,而指针变量有指针变量的运算方式
(后续讲解)。表达式
指的是一组运算数、运算符的组合,表达式一定具有值
,一个变量或一个常量可以是表达式,变量、常量和运算符也可以组成表达式,如:操作数
指的是参与运算
的值
或者对象
,如:操作数
的个数
,可以将运算符分为: 功能
,可以将运算符分为: NOTE
掌握一个运算符,需要关注以下几个方面:
IMPORTANT
普通变量支持上述的所有运算符;而指针变量并非支持上述的所有运算符,且支持运算符的含义和普通变量相差较大!!!
优先级 | 运算符 | 名称或含义 | 结合方向 |
---|---|---|---|
1 | [] | 数组下标 | ➡️(从左到右) |
() | 圆括号 | ||
. | 成员选择(对象) | ||
-> | 成员选择(指针) | ||
2 | - | 负号运算符 | ⬅️(从右到左) |
(类型) | 强制类型转换 | ||
++ | 自增运算符 | ||
-- | 自减运算符 | ||
* | 取值运算符 | ||
& | 取地址运算符 | ||
! | 逻辑非运算符 | ||
~ | 按位取反运算符 | ||
sizeof | 长度运算符 | ||
3 | / | 除 | ➡️(从左到右) |
* | 乘 | ||
% | 余数(取模) | ||
4 | + | 加 | ➡️(从左到右) |
- | 减 | ||
5 | << | 左移 | ➡️(从左到右) |
>> | 右移 | ||
6 | > | 大于 | ➡️(从左到右) |
>= | 大于等于 | ||
< | 小于 | ||
<= | 小于等于 | ||
7 | == | 等于 | ➡️(从左到右) |
!= | 不等于 | ||
8 | & | 按位与 | ➡️(从左到右) |
9 | ^ | 按位异或 | ➡️(从左到右) |
10 | | | 按位或 | ➡️(从左到右) |
11 | && | 逻辑与 | ➡️(从左到右) |
12 | || | 逻辑或 | ➡️(从左到右) |
13 | ?: | 条件运算符 | ⬅️(从右到左) |
14 | = | 赋值运算符 | ⬅️(从右到左) |
/= | 除后赋值 | ||
*= | 乘后赋值 | ||
%= | 取模后赋值 | ||
+= | 加后赋值 | ||
-= | 减后赋值 | ||
<<= | 左移后赋值 | ||
>>= | 右移后赋值 | ||
&= | 按位与后赋值 | ||
^= | 按位异或后赋值 | ||
|= | 按位或后赋值 | ||
15 | , | 逗号运算符 | ➡️(从左到右) |
WARNING
使用小括号来控制
表达式的执行顺序。分成几步
来完成。IMPORTANT
*
和取地址运算符 &
的优先级相同,并且运算方向都是从右向左!!!,
的优先级最低,并且运算方向是从左向右!!!在 CLion 中使用 GDB 调试时,可以通过反编译代码来查看指针变量和普通变量的区别。下面是具体的步骤:
确保在编译你的代码时使用了调试信息生成选项(如 -g
)。你可以在 CMakeLists.txt 文件中添加以下行:
set(CMAKE_CXX_FLAGS "\${CMAKE_CXX_FLAGS} -g")
当调试器在断点处暂停时,你可以在调试控制台中使用 GDB 命令来查看变量。以下是一些常用的 GDB 命令:
print variable_name
:打印变量的值。info locals
:打印当前作用域中的所有局部变量。whatis variable_name
:显示变量的类型。指针变量和普通变量的主要区别在于它们的类型和存储的内容。指针变量存储的是地址,而普通变量存储的是实际的值。通过 GDB 命令可以很容易地看到这种区别。
假设有如下代码:
#include <iostream>
int main() {
int a = 10;
int *p = &a;
std::cout << "a: " << a << ", p: " << p << std::endl;
return 0;
}
在 CLion 中设置断点并开始调试,程序将在 std::cout
行暂停。此时在调试控制台中输入以下命令:
print a
:输出变量 a
的值,应该是 10
。print p
:输出指针变量 p
的值,即 a
的地址。print *p
:输出指针 p
指向的值,即 10
。通过这些命令,你可以看到指针变量 p
实际上存储的是一个地址,而普通变量 a
存储的是一个整数值。
在某些情况下,你可能需要查看反汇编代码来更深入地理解变量的存储方式。使用以下命令可以查看当前函数的反汇编代码:
disassemble
:反汇编当前函数的代码。x/4wx &a
:查看变量 a
的内存内容。在 CLion 中使用 GDB 调试时,通过查看变量值和反汇编代码,可以清楚地区分指针变量和普通变量。指针变量存储地址,而普通变量存储实际的值,通过适当的 GDB 命令可以轻松辨别两者的区别。
在 C 语言中,普通变量
是直接存储数据
的变量
。对于普通变量,支持的操作包括:
int a = 5
。a + b
、a - b
、a * b
、a / b
a > b
、a == b
。a && b
、a || b
、!a
。a & b
、a | b
、a ^ b
、~a
、a << 2
、a >> 2
。a++
、--a
等。在 C 语言中,指针变量
存储的是另一个变量
的地址
。对于指针变量,支持的操作包括:
int *p = &a;
。*p = 10;
修改指向变量的值。p + 1
。p++
、p--
。ptr1 - ptr2
。p1 == p2
、p1 != p2
。*(p + i)
访问第 i
个元素。int **pp = &p;
。WARNING
在使用指针时,务必小心避免野指针和内存泄漏等问题。
在C语言中,同类指针相减的结果是一个整数,它表示两个指针之间相隔多少个指向的对象单位,而不是它们在内存中的字节偏移量。这种对象单位是指针所指向的具体类型的大小。
举个例子来说,如果你有两个指向整数数组元素的指针 p
和 q
,那么 p - q
的结果将是 p
指向的数组元素的索引与 q
指向的数组元素索引之间的差值。这个差值代表了在数组中相隔多少个整数元素,而不是它们在内存中的字节偏移量。
这种设计的优势在于,它使得指针运算更加直观和便于理解,特别是在处理数组和其他连续存储的数据结构时。因为指针运算结果的单位是根据指针所指向的具体类型来计算的,这样可以确保不同平台上的程序行为是一致的,不会受到底层硬件架构或者字节对齐规则的影响。
在C语言中,数组名和指针有很多相似之处,但数组名并不是指针变量。数组名实际是一个常量,它指向数组的第一个元素的地址。为了证明这一点,可以通过以下几个方面来说明:
数组名表示数组首地址: 数组名可以作为一个指针使用,数组名本身表示的是数组首地址。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 这句话是合法的,ptr现在指向arr[0]
printf("%p\\n", arr); // 打印数组名,会打印数组首地址
printf("%p\\n", &arr[0]); // 打印第一个元素的地址
数组名是常量指针: 数组名是一个常量指针,不能改变它指向的位置,而指针变量可以改变它指向的位置。
int arr[5];
int *ptr = arr; // 合法,ptr指向arr[0]
ptr++; // 合法,ptr现在指向arr[1]
// arr++; // 非法,编译错误,因为数组名是常量,不能改变
sizeof运算符的结果不同: 使用sizeof
运算符对数组名和指针变量会得到不同的结果,数组名会返回整个数组的大小,而指针变量会返回指针本身的大小。
int arr[5];
int *ptr = arr;
printf("sizeof(arr) = %lu\\n", sizeof(arr)); // 返回数组的大小,5 * sizeof(int)
printf("sizeof(ptr) = %lu\\n", sizeof(ptr)); // 返回指针的大小,通常是4或8字节
地址运算符的结果不同: 使用地址运算符&
对数组名和指针变量会得到不同的结果,对数组名使用&
会返回数组的地址,而对指针变量使用&
会返回指针变量本身的地址。
int arr[5];
int *ptr = arr;
printf("Address of array: %p\\n", &arr); // 返回整个数组的地址
printf("Address of pointer: %p\\n", &ptr); // 返回指针变量ptr的地址
综上所述,通过这些示例和解释,可以看出数组名虽然在某些场合下可以像指针一样使用,但它并不是一个真正的指针变量,而是一个常量,表示数组的首地址。
`,73),p=[o];function r(h,c,k,g,u,E){return t(),i("div",null,p)}const m=s(n,[["render",r]]);export{y as __pageData,m as default};