文章目录
- 1.内存和地址
- 1.1理解内存地址
- 酒店大堂:内存的入口
- 房间号:内存地址的意义
- 酒店的楼层划分:内存的结构
- 酒店的房间单位:计算机中的常见单位
- 1.2如何理解编址
- 2.指针变量和地址
- 2.1取地址操作符(`&`)
- 2.2 指针变量和解引用操作符(`*`)以及如何 拆解指针类型
- 2.3使用指针的好处
- 2.4指针变量的大小
- 为什么指针变量的大小固定?
- 3.指针变量类型的意义
- 指针变量的类型
- 指针的解引用
- 数据类型大小的差异
- 示例代码
- 3.2 指针与整数的加减操作
- 加法操作
- 减法操作
- 背后的原理
- 使用场景
- 3.3void*指针
- 4. const修饰指针
- 4.1`const`的使用
- 4.2.const修饰指针的应用场景
- 5.指针运算
- 5.1 指针与整数相加减
- 5.2指针之间的减法运算
- 5.3指针的关系运算
- 6.野指针
- 6.1 野指针的成因
- 未初始化指针
- 指针指向已释放的内存
- 指针越界
- 6.2 野指针的危害
- 6.3 规避野指针的策略
- 及时初始化指针
- 确保指针指向有效内存
- 及时释放指针指向的内存
- 避免指针越界
- 7. assert断言
- 7.1什么是断言?
- 7.2 assert宏的用法
- 7.3 assert的实践指南
- 7.4 示例代码
- 8.指针的使用和传址调用和传值调用
- 8.1strlen 函数的模拟实现
- 8.2传值调用和传址调用的示例:
- 小结
1.内存和地址
1.1理解内存地址
酒店大堂:内存的入口
当你走进一家酒店时,大堂是你首先到达的地方,正如CPU访问内存时,它通过内存地址找到数据的存放位置。每个内存地址都像是房间的门牌号,告诉CPU它需要的数据住在哪个“房间”里。
房间号:内存地址的意义
在这家酒店(内存)中,每个房间(内存单元)都有一个独一无二的号码(地址)。这些号码不仅帮助客人(CPU)找到他们的房间(数据),还揭示了酒店(内存)的组织结构。与酒店的房间号码一样,内存地址是顺序排列的,这让寻找连续的数据块变得容易。
酒店的楼层划分:内存的结构
酒店楼层被划分成了不同的区域,每个区域代表了内存中的一个特定部分。这种划分有助于提高酒店运营的效率,同样,内存划分也旨在优化计算机资源的使用。
- 前台区域(寄存器):这是最快速访问的区域,用于处理客人(数据)的即时请求。在内存中,这相当于CPU寄存器,提供了最快的数据访问速度。
- VIP区(缓存):位于前台附近的是VIP区,访问速度快于普通房间,用于存放短期内频繁访问的贵宾客人(数据)。这对应于计算机的缓存,它存储了近期将被频繁使用的数据,以减少访问主内存的时间。
- 客房区(主内存):酒店的主体部分,为大多数客人提供住宿服务。在计算机中,这相当于主内存,存储了当前正在使用的程序和数据。
- 地下室(硬盘/二级存储):虽然访问速度不如楼上的区域,地下室用于存放不常用但仍需要保留的物品。这在计算机中对应于硬盘或其他形式的二级存储,用于长期存储数据。
酒店的房间单位:计算机中的常见单位
在管理这家酒店时,了解不同房间的大小和类型是必要的。类似地,在计算机中,我们也有多种单位来衡量内存和数据的大小:
- 字节(Byte):最基本的存储单位,好比酒店中的一个储物柜,足以存放少量物品(数据)。
- 千字节(KB,Kilobyte):约等于1000字节,相当于一个小衣橱,可以存储更多的物品。
- 兆字节(MB,Megabyte):约等于1000千字节,像是一个小房间,用于存放更大量的物品。
- 吉字节(GB,Gigabyte):约等于1000兆字节,等同于一间大客房,能存储大量数据。
- 太字节(TB,Terabyte):约等于1000吉字节,好比酒店中的一整层楼,为存储巨量的数据提供空间。
内存单元的编号 == 地址== 指针
1.2如何理解编址
在这里硬件与硬件之间的协同合作是通过“线”来连接的,这里是被叫做地址总线。那么,什么是地址总线呢?
简单来说,地址总线可以理解为一系列用于传递电脉冲信号的线路。在地址总线上,每一根线路都可以承载一个电脉冲信号,这个信号通常是二进制的,也就是说它可以是有电(表示为1)或者无电(表示为0)。CPU通过这些有电和无电的信号组合来指定内存中的具体地址。
例如,假设有一个4位的地址总线,那么它可以有(2^4 = 16)种不同的信号组合,从0000到1111,分别可以用来指定内存中的16个不同的位置。CPU通过在这四条线路上发送特定的有电(1)和无电(0)的组合,来告诉内存它想要访问的确切位置。每一个含义都是一个地址。
地址信息被下达给内存,在内存上就可以找到与该地址相对应的数据,将数据通过数据总线转存到CPU寄存器。
理解了变量在内存中是如何存储的。现在我们可以知道:每个变量都有一个内存地址,这个地址可以通过使用&
运算符来获取。
#include <stdio.h>
int main()
{
int var = 5;
printf("变量的地址:%p\n", &var);
return 0;
}
2.指针变量和地址
2.1取地址操作符(&
)
取地址操作符&
是一个一元操作符,用于获取变量在内存中的地址。当你在一个变量前使用&
时,它返回该变量的内存地址。例如:
#include <stdio.h>
int main()
{
int var = 5;
printf("%d\n", var);
printf("变量的地址:%p\n", &var);
return 0;
}
先使用的是比较小的地址
2.2 指针变量和解引用操作符(*
)以及如何 拆解指针类型
指针变量是存储另一个变量地址的变量。它不同于一般的变量,指针变量指向的是位置,而不是值。声明一个指针变量时,你需要在其类型前加上*
符号。例如,int* ptr;
声明了一个指向整型的指针变量ptr
。
指针变量用于存储变量的地址,如下所示:
#include <stdio.h>
int main()
{
int var = 5;
int *ptr = &var;
printf("指针ptr存储的地址:%p\n", ptr);
printf("通过ptr访问var的值:%d\n", *ptr); // *ptr是解引用操作,得到存储在ptr地址的整型数据
return 0;
}
2.3使用指针的好处
使用指针的原因可以通过一个现实生活中的类比来更好地理解:
假设你有一本非常重要的书,你想与多个朋友分享。你有两个选项:一是给每个朋友复制一本书(相当于在程序中复制数据),这样做非常耗费资源(纸张、墨水等);二是告诉朋友们这本书在你家的确切位置(相当于使用指针),让他们自己来查阅。
以下是使用指针的几个主要优点:
-
节省资源:与复制完整数据相比,共享数据的位置(即地址)可以显著减少内存使用。在上述类比中,复制书籍消耗的资源要远多于仅仅共享书籍的位置。
-
提高效率:当需要在程序的不同部分或者不同函数间共享大量数据时,使用指针可以直接访问数据,而不是花时间和资源去复制数据。就像你告诉朋友们书的位置,他们可以直接访问,而不需要等待你复制并递送一本书给他们。
-
允许修改原数据:通过指针,函数可以直接修改原始数据的值,而不仅仅是它的副本。这就像你允许朋友在你的书上做标记,所有查阅这本书的人都能看到这些更改,而不是每个人都在不同的副本上做标记,这些更改互不影响。
-
支持动态数据结构:指针是实现诸如链表、树、图等动态数据结构的关键。这些结构在运行时可以扩展和缩减,需要指针来指向和链接其各个部分。就像在一个巨大的图书馆里,书籍可能不会按顺序排列,但你可以通过一个系统(指针)来找到每本书的确切位置。
-
实现复杂的数据结构:在C语言中,指针是实现数组、字符串和其他复杂数据结构的基础。通过指针,可以灵活地操作这些结构中的元素,就像你可以通过书架的索引系统来快速找到任何一本书一样滴。
所以容易看出指针是程序设计中的一种强大工具,它通过提供对内存直接访问的能力,使得资源管理更高效,数据处理更灵活。理解和掌握指针,就像学会在图书馆中熟练地找到任何一本书,是提高编程技能的重要一步!!!
2.4指针变量的大小
在讨论指针变量的大小时,我们实际上是在讨论指针变量本身占用的内存空间大小。这与指针指向的数据的大小无关,而是指存储内存地址所需要的空间大小。指针变量的大小取决于计算机的架构(32位或64位)和操作系统。
为什么指针变量的大小固定?
无论指针指向的是一个整数、一个字符还是一个复杂的结构体,指针本身的大小都是固定的。这是因为指针变量只存储内存地址,而内存地址的大小是由计算机的架构决定的。
-
32位系统中的指针:在32位系统中,地址空间是基于32位地址的,这意味着系统可以寻址(2^{32})个独立的内存地址。因此,在32位系统中,指针变量通常占用4字节(32位)的内存空间。
-
64位系统中的指针:相比之下,64位系统可以寻址(2^{64})个独立的内存地址,这大大增加了可用的内存空间。因此,在64位系统中,指针变量的大小通常是8字节(64位)。
3.指针变量类型的意义
指针变量的类型
虽然指针大小和指针类型无关,指针类型定义了指针指向的数据类型。但这对于解引用操作和指针算术运算来说非常重要,因为它决定了指针操作时应该考虑的内存大小。
int* ptr; // 指向整型的指针
char* cptr; // 指向字符的指针
每种类型的指针都有其对应的内存地址大小,但指向的数据类型决定了解引用时的行为。
指针的解引用
解引用是通过指针访问其指向地址中存储的数据的操作。使用解引用操作符*
来获取指针指向的值。
char*
和int*
是C语言中两种不同类型的指针,它们的主要区别在于所指向的数据类型及通过这些指针进行操作时内存中读取或修改数据的方式。char*
是指向字符数据的指针,而int*
是指向整型数据的指针。这一差异影响了指针运算和解引用操作的行为。
数据类型大小的差异
在大多数平台上,char
类型占用1字节,而int
类型通常占用4字节(这可能根据不同的系统或编译器有所不同)。这意味着当你通过这些指针进行加减运算时,指针的移动步长也会有所不同。
示例代码
让我们通过一个例子来演示这两种指针类型的区别:
#include <stdio.h>
int main()
{
int var = 0x11223344;
int* ptr = &var;
*ptr = 0;
return 0;
}
#include <stdio.h>
int main()
{
int var = 0x11223344;
char * ptr = &var;
*ptr = 0;
return 0;
}
容易看出int*能访问四个字节,而char解引用只能访问一个字节
3.2 指针与整数的加减操作
指针加上或减去一个整数会导致指针向前或向后移动一定数量的内存位置。这里的“一定数量”取决于指针指向的数据类型的大小。
加法操作
当我们对指针执行加法操作时,我们实际上是在移动指针。例如,如果我们有一个指向int
类型的指针,每个int
占用4个字节(这可能因编译器和平台而异),那么当我们对这个指针加2时,实际上是让指针向前移动了8个字节,指向了第三个整数。
#include <stdio.h>
int main()
{
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr; // 指向数组的第一个元素
ptr += 2; // 移动指针到第三个元素
printf("当前指针指向的值为: %d\n", *ptr); // 输出30
return 0;
}
减法操作
类似地,对指针执行减法操作会使指针向后移动。使用同样的int
指针例子,如果我们从指向第三个整数的指针中减去1,那么指针会移动回第二个整数的位置。
include <stdio.h>
int main()
{
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr + 2; // 初始时指向数组的第三个元素
ptr -= 1; // 移动指针回到第二个元素
printf("当前指针指向的值为: %d\n", *ptr); // 输出20
return 0;
}
指针的类型就是决定向前或向后一步能走多远的(距离)
背后的原理
指针加减整数的操作背后的原理基于内存地址的算术运算。当我们对指针加上(或减去)一个整数时,编译器会将这个整数乘以指针指向的数据类型的大小(以字节为单位),然后将结果加到(或从)指针的当前值上。这就是为什么指针的移动会依赖于它所指向的数据类型的大小。
使用场景
指针与整数的加减操作在多种场景下非常有用,例如遍历数组、动态内存管理等。
3.3void*指针
void
指针可以理解为无具体类型指针,(泛型指针)可以接收任意类型地址,但是不能进行指针的±整数和解引用的运算
在上面代码中,int类型的变量地址赋给char*类型的指针变量,会有如此警告
下面我们使用void*
指针接收地址
容易看出void*
可以接收不同类型的地址,但无法进行运算
一般是使用在函数参数的部分,用来接收不同类型数据的地址,达到泛型编程,使一个函数处理多种数据
在C语言中,const
修饰符用于声明常量,而当const
与指针结合使用时,产生了一些独特的语义和行为。本文将深入探讨const
修饰指针的技术性,并提供详细的代码示例,帮助读者更好地理解其应用与原理。
4. const修饰指针
4.1const
的使用
const
修饰指针的语法形式为:const int *ptr;
或int *const ptr;
。
第一种形式表示指针指向的数据是常量,第二种形式表示指针本身是常量。
- 指向常量的指针:指针指向的数据是常量,不能通过指针修改该数据。
指向内容不可修改,指向对象可以修改
const int num = 10;
const int *ptr = #
//*ptr = 20; // 错误:无法修改指针所指向的数据
//ptr = &another_num//正确;可以修改指针指向
- 常量指针:指针本身是常量,不能修改指针的指向。
int num = 10;
int *const ptr = #
//*ptr = 20; // 正确:可以修改指针所指向的数据
//ptr = &another_num; // 错误:无法修改指针本身的指向
4.2.const修饰指针的应用场景
- 函数参数中的常量指针:用于传递不希望修改的数据,同时保证函数内部不会修改该数据。
void print_data(const int *ptr)
{
printf("Value: %d\n", *ptr);
}
int main()
{
int num = 10;
print_data(&num);
return 0;
}
- 常量指针与字符串常量:常量指针经常用于指向字符串常量,以保护字符串数据不被修改。
const char *str = "Hello, world!";
// *str = 'h'; // 错误:无法修改指向的字符串数据
在c艹中const修饰的彻底是常量
5.指针运算
5.1 指针与整数相加减
指针与整数相加减是指针运算中最基本的操作之一,它允许程序员在内存中进行灵活的定位和偏移。
#include <stdio.h>
int main()
{
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr; // 指向数组的第一个元素
// 指针加整数,实现指针偏移
ptr = ptr + 2; // ptr现在指向数组的第三个元素
printf("Value: %d\n", *ptr); // 输出30
// 指针减整数,实现指针偏移
ptr = ptr - 1; // ptr现在指向数组的第二个元素
printf("Value: %d\n", *ptr); // 输出20
return 0;
}
在这个示例中,指针 ptr
指向了一个整型数组 arr
的第一个元素。通过将指针与整数相加减,可以实现对指针的偏移,从而实现对数组中不同位置的访问。
上面已经讲过了,这里只是再提一下
5.2指针之间的减法运算
指针之间的减法运算可以得到它们之间的距离(以元素个数为单位),这对于计算数组中元素的个数或者实现迭代器等功能十分有用。
#include <stdio.h>
int main()
{
int arr[] = {10, 20, 30, 40, 50};
int *ptr1 = &arr[0]; // 指向数组的第一个元素
int *ptr2 = &arr[3]; // 指向数组的第四个元素
// 指针相减,得到指针之间的距离(以元素个数为单位)
int distance = ptr2 - ptr1;
printf("Distance: %d\n", distance); // 输出3,即ptr2与ptr1之间相隔3个元素
return 0;
}
在这个示例中,通过指针 ptr1
和 ptr2
的减法运算,得到它们之间相隔的元素个数,即数组中的距离。
5.3指针的关系运算
指针之间还可以进行关系运算,包括相等性比较和大小比较。
#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *ptr1 = &arr[1];
int *ptr2 = &arr[3];
// 指针相等性比较
if (ptr1 == ptr2)
{
printf("Pointers are equal.\n");
} else {
printf("Pointers are not equal.\n");
}
// 指针大小比较
if (ptr1 < ptr2)
{
printf("ptr1 is less than ptr2.\n");
}
else
{
printf("ptr1 is not less than ptr2.\n");
}
return 0;
}
在这个示例中,指针 ptr1
和 ptr2
分别指向数组中的不同元素。通过关系运算符,可以判断指针之间的相等性和大小关系。
6.野指针
6.1 野指针的成因
未初始化指针
int *ptr;
printf("%d", *ptr); // 未初始化的指针ptr被解引用,指向未知内存地址
指针指向已释放的内存
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr = (int *)malloc(sizeof(int)); // 分配内存空间
if (ptr != NULL)
{
*ptr = 10; // 向分配的内存空间写入数据
printf("Value: %d\n", *ptr); // 输出数据
free(ptr); // 释放内存空间
ptr = NULL; // 将指针置为NULL,防止成为野指针
printf("Value: %d\n", *ptr); // 试图访问已经释放的内存空间
}
return 0;
}
在这个例子中,指针 ptr
在使用 malloc()
分配了内存空间后,被用来存储一个整数值,并通过 printf()
函数输出了该值。然后,通过 free()
函数释放了 ptr
所指向的内存空间,并将 ptr
设置为 NULL
。最后,尝试再次使用 printf()
输出 ptr
所指向的值,此时 ptr
已经成为了野指针,因为它指向的内存空间已经被释放,这可能导致程序出现不可预测的行为。
指针越界
int *get_pointer()
{
int num = 10;
int *ptr = #
return ptr; // 返回了一个局部变量的指针
}
int main()
{
int *ptr = get_pointer();
printf("%d", *ptr); // ptr指向的内存超出了作用域,成为了野指针
return 0;
}
6.2 野指针的危害
- 野指针可能导致程序崩溃或产生不可预测的行为。
- 在调试过程中,野指针可能会给程序员带来困惑和耗费大量的时间来定位问题的根源。
- 野指针可能会导致内存泄漏或内存损坏,从而影响程序的性能和稳定性。
6.3 规避野指针的策略
及时初始化指针
int *ptr = NULL; // 显式初始化指针为空指针
确保指针指向有效内存
int *ptr = (int *)malloc(sizeof(int));
if (ptr != NULL)
{
// 分配内存成功后再进行操作
}
及时释放指针指向的内存
int *ptr = (int *)malloc(sizeof(int));
if (ptr != NULL)
{
free(ptr);
ptr = NULL; // 释放内存后将指针置为空指针
}
避免指针越界
int *get_pointer()
{
int num = 10;
int *ptr = #
return ptr; // 返回局部变量的指针会导致野指针
}
7. assert断言
assert
宏的用法、原理和实践指南
7.1什么是断言?
在C语言中,断言是一种在程序中加入的检查点,用于检查程序的假设是否成立。
如果假设不成立,则断言将导致程序终止并输出相关的错误信息,帮助程序员定位问题所在。
7.2 assert宏的用法
assert
宏定义在<assert.h>
头文件中,其基本语法如下:
#include <assert.h>
assert(expression);
其中,expression
是一个表达式,如果表达式的值为假(即0),则assert
宏将终止程序的执行,并输出相应的错误信息。
7.3 assert的实践指南
-
合理选择断言的位置:将断言放置在程序中的关键位置,如函数入口、循环体内部、关键计算或数据操作之前。
-
明确表达断言的意图:断言的表达式应该简洁明了,能够清晰地表达程序的假设或条件。
-
避免副作用:在断言中避免引入副作用,以免影响程序的正常运行。
-
编译时开启断言:在开发和测试阶段,建议将断言开启,以便及时发现和解决问题。
-
谨慎使用断言:断言应该用于检测程序的不变条件和可预期的错误情况,而不是用于处理运行时错误或非预期的情况。
7.4 示例代码
#include <stdio.h>
#include <assert.h>
int main()
{
int x = 10;
int y = 20;
// 检查假设:x 应该小于 y
assert(x < y);
printf("Assertion passed: x is less than y\n");
return 0;
}
在这个示例中,断言检查了表达式 x < y
是否成立,如果不成立,则程序终止并输出相应的错误信息;否则,输出“Assertion passed: x is less than y”。
如果不需要了,在#include <assert.h>
加上desine NDBUG
,编译器就会禁用所有assert()
语句。
8.指针的使用和传址调用和传值调用
8.1strlen 函数的模拟实现
strlen 函数用于计算字符串的长度,它计算的是/0之前的长度,并返回字符串的长度。以下是 strlen 函数的简化模拟实现:
#include <stdio.h>
// 模拟实现 strlen 函数
size_t myStrlen(const char *str)
{
const char *ptr = str;
while (*ptr != '\0')
{
ptr++;
}
return ptr - str;
}
int main() {
char str[] = "Hello, world!";
size_t len = myStrlen(str);
printf("Length of '%s' is %zu\n", str, len);
return 0;
}
在这个示例中,myStrlen 函数通过遍历字符串直到遇到终止符 \0 来计算字符串的长度,并返回结果。
8.2传值调用和传址调用的示例:
很容易看出x和a,y和b的地址不一样,xy是独立的空间,在swap1里面交换x和y。不会影响a和b,swap1在使用的时候是直接吧变量本身传给了函数,这就是传值调用。
形参是实参的一份临时拷贝,对形参修改不影响实参。
swap1是失败的
传址调用可以让函数和主调函数之间建立真正的联系,在函数内部修改主调函数的变量
小结
所以未来函数中只是需要主调函数的变量来实现计算,就可以传值,若要修改主调函数中的变量的值,就需要传址调用。
——end(…)