CPP指针深析
指针对于初学者往往是很难以琢磨的东西,因为它并不如变量那么抽象,而是更贴近底层的真实结构。指针操作往往会出现各种各样的岔子,最常见的便是”segmentation fault”。所以这里辨析了各种指针类型,实践出真知,下面的例子如果自己敲下来就更好了😁😁😁
指针是C++中强大的工具,灵活但容易出错,同时也是一把“双刃剑”。
1. 指针的基本概念
指针是一种保存地址的变量。
int a = 10;
int* p = &a; // p保存a的地址
std::cout << "Value of a: " << *p << std::endl; // 解引用获取a的值
&
:取地址符,获取变量地址。*
:解引用符,通过地址访问变量值。
注意:未初始化的指针称为野指针,会导致未定义行为。
int* p; // 未初始化
std::cout << *p << std::endl; // 未定义行为
2. 指针的分类与使用
2.1 空指针
空指针(nullptr
)指向“无效”地址,避免野指针问题。
int* p = nullptr;
if (p == nullptr) {
std::cout << "Pointer is null" << std::endl;
}
注意:解引用空指针会引发运行时错误。
2.2 常量指针和指针常量
- 常量指针:指向的值不能修改。
const int a = 10;
const int* p = &a;
//*p = 20; // 错误,值不可修改
p = nullptr; // 指针本身可以指向其他地址
- 指针常量:指针的地址不能修改。
int b = 10, c = 20;
int* const p = &b;
//p = &c; // 错误,指针地址不可修改
*p = 30; // 正确,值可以修改
- 指向常量的指针常量:
const int a = 10;
const int* const p = &a;
//*p = 20; // 错误
//p = &b; // 错误
2.3 指针与数组
指针与数组关系密切,数组名即为首元素地址:
int arr[] = {1, 2, 3};
int* p = arr;
std::cout << p[1] << std::endl; // 输出 2
使用指针操作数组:
for (int i = 0; i < 3; ++i) {
std::cout << *(p + i) << " "; // 依次输出数组元素
}
2.4 指针与函数
指针可用于传递函数参数,实现高效操作:
void swap(int* x, int* y) {
int temp = *x;
*x = *y;
*y = temp;
}
int a = 10, b = 20;
swap(&a, &b);
std::cout << a << " " << b << std::endl; // 输出 20 10
2.5 动态内存分配与指针
通过new
和delete
动态分配内存:
int* p = new int(42);
std::cout << *p << std::endl; // 输出 42
delete p; // 释放内存
动态数组:
int* arr = new int[5];
for (int i = 0; i < 5; ++i) arr[i] = i + 1;
delete[] arr; // 释放动态数组
注意:
- 忘记释放内存会导致内存泄漏。
- 使用
delete
释放后,不要再解引用。
2.6 指针与多级指针
多级指针是指向指针的指针:
int a = 10;
int* p1 = &a;
int** p2 = &p1; // p2指向p1
std::cout << **p2 << std::endl; // 输出10
3. 常见问题与调试技巧
空指针解引用
int* p = nullptr; std::cout << *p << std::endl; // segmentation fault
指针越界
int arr[3] = {1, 2, 3}; int* p = arr + 3; // 超出数组范围 std::cout << *p << std::endl; // 未定义行为
野指针
int* p; delete p; // p未指向有效地址
调试方法:
- 检查
nullptr
。 - 使用工具(如Valgrind)检测内存泄漏。
- 避免无意义的解引用操作。
4. 总结
指针的强大功能伴随着复杂性和风险。通过反复实践和注重细节,可以掌握指针的精髓,写出高效可靠的C++代码。初学者特别要注意避免未定义行为,同时养成良好的编码习惯。
const修饰指针
#include <iostream>
using namespace std;
class Rectangle{
public:
Rectangle(int length, int width){chang=length; kuan=width;}
void SetLength(int length){ chang=length;}
int GetLength() const{ return chang;}
private:
int chang;
int kuan;
};
int main(){
// int a=123;
// const int *p=&a;//const 修饰*,p可以改
// cout<<*p<<endl;
// int b=567;
// p=&b;
// cout<<*p<<endl;
// int const *p1=&a;
// int * const p2=&b;const 修饰p2,*p2可以改
// *p2=789;
// // p2=&a;
// cout<<*p2<<endl;
Rectangle *rec=new Rectangle(123, 456);
Rectangle *rec1=new Rectangle(789, 456);
cout<<rec->GetLength()<<endl;
const Rectangle * pRec=rec;
cout<<pRec->GetLength()<<endl;
pRec=rec1;//同p
cout<<pRec->GetLength()<<endl;//只能调用const方法
Rectangle * const pRec1=rec;//同p2
cout<<pRec1->GetLength()<<endl;
Rectangle *pPrec0=rec;
pPrec0->SetLength(789);//非const方法也可以调用
cout<<pRec->GetLength()<<endl;
}
这段代码通过一些实例讲解了const
修饰指针的不同用法,并结合类的const
方法,展示了指针常量和指向常量的指针的区别。以下是代码运行的核心分析:
核心概念解析
1. const
修饰指针的两种情况
指向常量的指针(
const
修饰指向内容)- 指针可以指向不同地址,但指向的内容不可修改。
const int *p = &a; // 或 int const *p = &a; p = &b; // OK: 修改指针地址 *p = 10; // 错误: 修改指向内容
指针常量(
const
修饰指针本身)- 指针地址不可修改,但指向的内容可以改。
int *const p = &a; p = &b; // 错误: 修改指针地址 *p = 10; // OK: 修改指向内容
2. 类中的const
方法
const
方法:方法后加
const
,表示该方法不会修改类的成员变量。int GetLength() const; // 只读方法
如果通过
const
对象(或const
指针)调用方法,只能调用const
方法。
代码行为解析
(1)创建普通对象和指针
Rectangle *rec = new Rectangle(123, 456);
Rectangle *rec1 = new Rectangle(789, 456);
cout << rec->GetLength() << endl; // 输出 123
rec
是普通指针,能自由调用非const
方法。
(2)指向常量的指针
const Rectangle *pRec = rec; // pRec指向一个常量对象
cout << pRec->GetLength() << endl; // 输出 123
pRec = rec1; // OK: 修改指针地址
cout << pRec->GetLength() << endl; // 输出 789
// pRec->SetLength(456); // 错误: 无法调用非const方法
pRec
只能调用const
方法(GetLength
),但可以指向其他Rectangle
对象。
(3)指针常量
Rectangle *const pRec1 = rec; // pRec1为指针常量
cout << pRec1->GetLength() << endl; // 输出 123
pRec1->SetLength(789); // OK: 可调用非const方法修改内容
cout << pRec1->GetLength() << endl; // 输出 789
// pRec1 = rec1; // 错误: 无法修改指针地址
pRec1
的指向地址不可修改,但其内容可以更改。
(4)普通指针调用非const
方法
Rectangle *pPrec0 = rec;
pPrec0->SetLength(789); // 调用非const方法
cout << pRec->GetLength() << endl; // 输出 789
pPrec0
是普通指针,可以自由调用非const
方法。
输出结果
最终输出如下:
123
123
789
123
789
总结
const
修饰指针的用法: 分清指向内容和指针本身谁是常量。- 类的
const
方法: 适用于保护类状态,在需要只读访问时尤为重要。 - 实践建议: 在需要只读访问的场景下,优先使用
const
指针和const
方法,提高代码的安全性和可维护性。
一维数组指针的区别
a代表首地址,在sizeof和&a时当成了数组整体
#include <iostream>
using namespace std;
#define sz(type) cout<<sizeof(type)<<endl;
int main(){
int a[3]={12,13,14};
sz(a);
cout<<a<<endl;
cout<<&a<<endl;
cout<<&a[0]<<endl;
/*
12
0x9ffdb4
0x9ffdb4
0x9ffdb4
*/
cout<<endl;
cout<<a<<" "<<a+1<<" "<<a+2<<endl; //a = &a[0] != &a
cout<<&a<<" "<<&a+1<<" "<<&a+2<<endl;
cout<<&a[0]<<" "<<&a[0]+1<<" "<<&a[0]+2<<endl;
/*
0x9ffdb4 0x9ffdb8 0x9ffdbc //步长是4,一个int
0x9ffdb4 0x9ffdc0 0x9ffdcc //步长是12,三个int,也就是数组长度
0x9ffdb4 0x9ffdb8 0x9ffdbc //步长是4,一个int
*/
return 0;
}
这个代码的核心在于区分一维数组的 数组名 和 数组地址 的概念,并通过sizeof
和地址输出展示了它们的差异。下面详细解析这段代码及其输出。
核心概念解析
1. 数组名 a
- 数组名(如
a
)是一个特殊的指针,默认指向数组的首元素,即&a[0]
。 - 但在某些特定场景中,
a
表现为整个数组的标志,比如sizeof(a)
和&a
。
2. sizeof
的作用
sizeof(a)
:计算数组整体的字节大小(数组长度 × 元素大小)。sizeof(&a)
:计算指向数组的指针的大小,与平台有关(通常是4字节或8字节)。
3. &a
和 a
的区别
a
是指向首元素的指针,等同于&a[0]
。&a
是整个数组的地址,表示一个数组整体。
两者类型不同:
a
的类型是int*
,步长是单个元素的大小(sizeof(int)
)。&a
的类型是int(*)[3]
,步长是整个数组的大小(sizeof(a)
)。
4. 地址运算
a + i
:表示第i
个元素的地址。&a + i
:表示第i
个数组的地址,步长是数组大小(即 3 ×sizeof(int)
)。&a[0] + i
:等价于a + i
,表示第i
个元素的地址。
代码解析及输出
sizeof
示例
sz(a); // 数组整体大小:3 × sizeof(int) = 12(假设int为4字节)
输出:
12
地址比较
cout << a << endl; // 数组首元素地址:&a[0]
cout << &a << endl; // 数组整体地址
cout << &a[0] << endl; // 数组首元素地址:&a[0]
输出:
0x9ffdb4 // 假设是数组的首地址,a 等价于 &a[0]
0x9ffdb4 // &a 表示数组整体的地址,与首地址相同,但意义不同
0x9ffdb4 // &a[0] 是首元素地址
地址偏移
cout << a << " " << a+1 << " " << a+2 << endl; // 步长是单个元素的大小
cout << &a << " " << &a+1 << " " << &a+2 << endl; // 步长是整个数组的大小
cout << &a[0] << " " << &a[0]+1 << " " << &a[0]+2 << endl; // 步长是单个元素的大小
输出:
0x9ffdb4 0x9ffdb8 0x9ffdbc // a,步长是4字节(1个int大小)
0x9ffdb4 0x9ffdc0 0x9ffdcc // &a,步长是12字节(整个数组大小)
0x9ffdb4 0x9ffdb8 0x9ffdbc // &a[0],步长是4字节(1个int大小)
总结
a
与&a
的区别:a
是指向首元素的指针,类型是int*
,步长是单个元素的大小。&a
是指向整个数组的指针,类型是int(*)[3]
,步长是整个数组的大小。
sizeof
的结果:sizeof(a)
是数组整体大小,等于数组长度 × 单元素大小
。sizeof(&a)
是指针大小,通常是 4 或 8 字节。
- 地址运算步长:
a + 1
或&a[0] + 1
,步长是单个元素的大小(sizeof(int)
)。&a + 1
,步长是整个数组的大小(sizeof(a)
)。
通过这些例子可以清楚地理解一维数组在指针操作中的差异,更好地避免误用。
二维数组指针的区别
注意与一维指针类比区别开来。
指针有减法和比较运算,没有加法运算。
#include <iostream>
using namespace std;
#define sz(type) cout<<sizeof(type)<<endl;
int main(){
int a[2][3]={
{12,13,14},
{112,113,114}
};
sz(a);
sz(a[0]);
sz(a[0][0]);
cout<<a<<endl;
cout<<&a[0]<<endl;
cout<<a[0]<<endl;
cout<<&a[0][0]<<endl;
cout<<endl;//步长是三个元素的数组的长度
cout<<"a: "<<a<<" "<<a+1<<endl; //a = &a[0]
cout<<"&a[0]:"<<&a[0]<<" "<<&a[0]+1<<endl;
cout<<endl;//步长是一个int
cout<<"a[0]: "<<a[0]<<" "<<a[0]+1<<endl; //a[0] = &a[0][0]
cout<<"&a[0][0]: "<<&a[0][0]<<" "<<&a[0][0]+1<<endl;
cout<<endl;
cout<<"&a: "<<&a<<" "<<&a+1<<endl;//步长是二维数组的长度
return 0;
}
这段代码分析了二维数组中的指针及其不同地址步长的含义。以下从二维数组的本质入手,结合代码详细解析指针在二维数组中的行为及其与一维数组的区别。
二维数组指针核心概念
1. 二维数组的内存布局
二维数组本质上是连续的内存块,可以看成是若干行一维数组的集合。
例如,int a[2][3]
表示有两行,每行三个整型元素。
a[2][3] = { {12, 13, 14}, {112, 113, 114} }
内存布局: [12, 13, 14, 112, 113, 114]
a[i]
是指向第i
行(即一维数组)的指针。a[i][j]
是指向第i
行第j
列的具体元素。
2. 各指针的含义
表达式 | 含义 | 类型 |
---|---|---|
a |
二维数组首地址,等价于 &a[0] |
int (*)[3] |
a[0] |
第一行首地址,等价于 &a[0][0] |
int* |
&a[0] |
第一行的地址(整体的地址) | int (*)[3] |
&a[0][0] |
第一行第一个元素的地址 | int* |
&a |
整个二维数组的地址 | int (*)[2][3] |
3. 步长的区别
a
或&a[0]
的步长:sizeof(a[0])
,即一行的大小,等于3 × sizeof(int)
。a[0]
或&a[0][0]
的步长:sizeof(int)
,即单个元素的大小。&a
的步长:sizeof(a)
,即整个二维数组的大小,等于2 × 3 × sizeof(int)
。
代码解析与输出
1. 使用 sizeof
查看大小
sz(a); // 整个二维数组的大小: 2 × 3 × sizeof(int) = 24(假设 int 是 4 字节)
sz(a[0]); // 一行的大小: 3 × sizeof(int) = 12
sz(a[0][0]); // 单个元素的大小: sizeof(int) = 4
输出:
24
12
4
2. 各指针的地址输出
cout << a << endl; // 等价于 &a[0],指向第一行
cout << &a[0] << endl; // 等价于 a
cout << a[0] << endl; // 等价于 &a[0][0],指向第一行首元素
cout << &a[0][0] << endl; // 第一行首元素地址
输出:
0x9ffdb4 // 数组首地址
0x9ffdb4 // 等价于 &a[0]
0x9ffdb4 // 第一行首元素地址
0x9ffdb4 // 第一行首元素地址
3. 步长分析
(1) a
和 &a[0]
的步长
cout << "a: " << a << " " << a + 1 << endl;
cout << "&a[0]:" << &a[0] << " " << &a[0] + 1 << endl;
a + 1
或&a[0] + 1
:跳过一行,步长是3 × sizeof(int)
。
输出:
a: 0x9ffdb4 0x9ffdc0 // 步长是12字节(一行大小)
&a[0]: 0x9ffdb4 0x9ffdc0
(2) a[0]
和 &a[0][0]
的步长
cout << "a[0]: " << a[0] << " " << a[0] + 1 << endl;
cout << "&a[0][0]: " << &a[0][0] << " " << &a[0][0] + 1 << endl;
a[0] + 1
或&a[0][0] + 1
:跳过一个元素,步长是sizeof(int)
。
输出:
a[0]: 0x9ffdb4 0x9ffdb8 // 步长是4字节(一个int大小)
&a[0][0]: 0x9ffdb4 0x9ffdb8
(3) &a
的步长
cout << "&a: " << &a << " " << &a + 1 << endl;
&a + 1
:跳过整个二维数组,步长是sizeof(a)
。
输出:
&a: 0x9ffdb4 0x9ffdd8 // 步长是24字节(整个数组大小)
与一维数组的区别
属性 | 一维数组 | 二维数组 |
---|---|---|
数组名类型 | int* |
int (*)[列数] |
a + 1 步长 |
一个元素大小 | 一行的大小 |
&a + 1 步长 |
整个数组大小 | 整个二维数组的大小 |
行地址 | 不适用 | &a[行号] 表示行的整体地址 |
元素地址 | &a[索引] |
&a[行号][列号] 表示元素地址 |
总结
- 二维数组的数组名:
a
是指向第0行的指针,类型为int (*)[列数]
,步长是单行大小。 - 行与元素的指针:
a[i]
是指向第i
行首元素的指针,类型为int*
。&a[i]
是第i
行整体的地址,类型为int (*)[列数]
。
- 地址运算步长:
a + 1
或&a[0] + 1
跳过一行,步长是列数 × 元素大小
。&a + 1
跳过整个二维数组,步长是行数 × 列数 × 元素大小
。
指针作为函数的参数
https://blog.csdn.net/ykm0722/article/details/7031387
使用swap(交换两个变量的值)作为演示,实现Swap的四种方法。当数组名作为函数的参数来传递的时候,他的高贵的数组结构特性已经失去了,成了一个地地道道的只拥有4个字节的平民。
#include <iostream>
using namespace std;
#define SWAP(a, b, temp) (temp=a, a=b, b=temp);
void func(int *x, int *y){
int z=*x;
*x=*y;
*y=z;
}
void func1(int &x, int &y){
cout<<x<<" "<<y<<endl;
int z=x;
x=y;
y=z;
}
template<class T>
void Swap(T &a, T&b){
T temp=a;
a=b;
b=temp;
}
int main(){
float a=12.1, b=13.2;
cout<<a<<" "<<b<<endl;
// int c=a;
// a=b;
// b=c;
float c;
Swap(a, b);
cout<<a<<" "<<b<<endl;
return 0;
}
这段代码展示了使用指针作为函数参数以及多种方式实现 swap
函数的机制和细节。以下是对代码和相关概念的详细解析。
1. 多种实现 swap
的方式
(1) 宏定义实现
#define SWAP(a, b, temp) (temp=a, a=b, b=temp);
- 使用 宏定义 的方式实现变量交换。
- 优点:代码简单直观。
- 缺点:
- 缺乏类型安全性:没有检查参数类型,一旦传递复杂类型可能出错。
- 调试困难:宏替换发生在预处理阶段,容易导致调试不便。
示例:
float a = 12.1, b = 13.2, temp;
SWAP(a, b, temp);
(2) 使用指针参数
void func(int *x, int *y){
int z = *x;
*x = *y;
*y = z;
}
- 通过指针传递变量地址,实现值交换。
- 优点:函数可以直接操作实参的值,而非副本。
- 缺点:调用时需要传递地址,使用稍显繁琐。
调用示例:
int a = 5, b = 10;
func(&a, &b);
(3) 使用引用参数
void func1(int &x, int &y){
int z = x;
x = y;
y = z;
}
- 通过 引用传递,实现值交换。
- 优点:
- 代码更简洁,调用方式与值传递类似(不需要显式取地址)。
- 不需要额外的解引用操作,性能略优。
- 缺点:不支持引用常量或数组。
调用示例:
int a = 5, b = 10;
func1(a, b);
(4) 使用模板
template<class T>
void Swap(T &a, T &b){
T temp = a;
a = b;
b = temp;
}
- 使用 C++模板 泛化
swap
函数,支持多种数据类型。 - 优点:
- 代码复用性强,适用于任何支持赋值操作的类型(如整型、浮点型、类对象等)。
- 类型安全。
- 缺点:略增加编译时间。
调用示例:
float a = 12.1, b = 13.2;
Swap(a, b);
2. 特殊点:数组名作为函数参数
在 C++ 中,数组名作为函数参数传递时,会退化为指针。以下是具体表现:
数组名的特性
- 数组名本质:数组名是数组首元素的地址,但它保留了数组的整体大小信息。
- 传递到函数中:当数组名作为参数时,它退化为普通指针,数组大小信息丢失。
示例代码
void processArray(int arr[]) {
cout << sizeof(arr) << endl; // 输出指针大小
}
int main() {
int arr[10] = {0};
cout << sizeof(arr) << endl; // 输出数组大小 (40)
processArray(arr); // 数组退化为指针 (4或8)
}
输出
40 // 数组实际大小:10 × sizeof(int)
4 // 指针大小(32位系统)或 8(64位系统)
结论
- 在函数内无法通过
sizeof
获取数组的长度。 - 如果需要数组长度,通常需要显式传递数组大小作为参数。
3. 代码解析与输出
(1) 宏交换 SWAP
float a = 12.1, b = 13.2, temp;
SWAP(a, b, temp);
cout << a << " " << b << endl;
输出:
13.2 12.1
(2) 指针传递
int a = 5, b = 10;
func(&a, &b);
cout << a << " " << b << endl;
输出:
10 5
(3) 引用传递
int a = 5, b = 10;
func1(a, b);
cout << a << " " << b << endl;
输出:
10 5
(4) 模板函数
float a = 12.1, b = 13.2;
Swap(a, b);
cout << a << " " << b << endl;
输出:
13.2 12.1
总结
- 宏定义:代码简单,但缺乏类型安全,不推荐。
- 指针传递:显式操作地址,适合 C 风格代码。
- 引用传递:代码更现代化,推荐使用。
- 模板:支持泛型交换,适用于复杂类型,是最通用的方式。
通过这些实现方式,可以灵活选择适合需求的方案,同时理解数组在函数参数中退化为指针的行为,有助于写出更清晰、可靠的代码。
指针作为返回值
要作为返回值,必须保证调用后返回的指针不会销毁,否则就是野指针。
#include <iostream>
using namespace std;
int a[3]; // 全局数组,自动初始化为0
int* func() {
return a; // 返回全局变量的地址
}
int main() {
int a = 1; // 局部变量
cout << func()[0] << " " << func()[1] << endl;
return 0;
}
这段代码主要展示了指针作为函数返回值的机制,并且提到了一个非常关键的问题——返回指针的生命周期。以下是详细解析。
1. 指针作为返回值的关键点
- 指针返回值是函数返回一个地址,调用者可以通过这个地址访问存储在该位置的值。
- 安全性问题:如果返回的指针指向了函数的局部变量,则在函数返回后,该局部变量会被销毁,导致指针成为野指针,可能引发不可预期的错误。
2. 示例代码解析
2.1 全局数组的特点
- 全局变量的生命周期
- 全局变量的生命周期贯穿整个程序运行周期,直到程序结束才会被销毁。
- 因此返回全局变量的地址是安全的,不会出现野指针。
- 全局数组的初始化
- 如果没有显式赋值,全局数组中的元素会被初始化为
0
(默认值)。 - 这里
int a[3] = {0, 0, 0}
。
- 如果没有显式赋值,全局数组中的元素会被初始化为
2.2 函数 func()
的返回值
func()
返回全局数组a
的首地址。- 在主函数
main()
中,通过func()
可以访问全局数组的内容。 - 优点:返回全局数组的地址是安全的。
- 缺点:如果多个地方修改了同一全局变量,会导致数据不可预测。
2.3 局部变量作为返回值的风险
以下是一个反面例子:
int* unsafeFunc() {
int localVar = 42; // 局部变量
return &localVar; // 返回局部变量地址
}
调用时:
int* ptr = unsafeFunc();
cout << *ptr << endl; // 未定义行为
- 问题:
localVar
是函数的局部变量,在函数返回时,它会被销毁。- 返回的地址指向一个已经被释放的内存区域,结果是未定义行为。
2.4 全局变量的安全返回
代码:
int* func() {
return a;
}
- 安全性:返回的是全局数组的地址,不会有生命周期问题。
- 数据访问:
func()[0]
等价于访问全局数组a[0]
。- 默认值
0
会被输出。
3. 输出结果
int a[3]; // 全局数组,初始化为 {0, 0, 0}
int main() {
cout << func()[0] << " " << func()[1] << endl;
return 0;
}
输出结果:
0 0
4. 常见场景及建议
4.1 返回全局变量指针
适用场景:
- 需要函数返回一个共享的全局数据结构。
- 确保所有访问点都使用相同的全局数据。
注意:
- 全局变量的使用需要小心,容易导致代码耦合高、可维护性低。
4.2 返回动态分配的内存
如果需要函数动态生成数据,可以使用动态分配:
int* createArray() {
int* arr = new int[3]{1, 2, 3}; // 动态分配数组
return arr; // 返回数组首地址
}
int main() {
int* arr = createArray();
cout << arr[0] << " " << arr[1] << " " << arr[2] << endl;
delete[] arr; // 使用完毕释放内存
return 0;
}
优点:
- 数据独立于函数栈帧,生命周期可控。
- 灵活性高,可在堆上存储大数据。
注意:
- 调用者负责释放动态分配的内存,否则可能导致内存泄漏。
4.3 返回局部静态变量的指针
静态变量具有全局生命周期,但作用域受限于定义所在的函数。
int* func() {
static int localStaticVar = 42;
return &localStaticVar; // 返回局部静态变量地址
}
int main() {
int* ptr = func();
cout << *ptr << endl; // 安全访问
return 0;
}
优点:
- 静态变量不会被销毁,返回地址是安全的。
- 避免了全局变量的污染。
5. 总结
- 返回值类型分析:
- 全局变量:安全,但可能引入全局状态问题。
- 局部变量:危险,容易导致野指针。
- 动态分配内存:安全,但需手动管理内存。
- 静态变量:安全,但需注意多线程安全问题。
- 建议:
- 尽量避免使用全局变量。
- 优先考虑返回动态分配的内存或局部静态变量。
- 在多线程环境下,需注意同步访问问题。
通过理解指针生命周期和作用域,能更好地写出健壮和高效的代码。
字符数组指针
1、str和p是不相同的!!
#include <iostream>
#include <cstring>
int main() {
char str[3]="ab";
char *p=str;//此处可以修改str,因为str只是字符数组(使用了字符串字面量来初始化str),但是并不是常量
size_t length = std::strlen(str);//遇到'\0'才会停止计数
// 例如 char a[3]={'a', 'b', 'c'}, 使用strlen后的长度不是3!!!
printf("%c-%s-%ld-%ld\n", *str, str, sizeof(str), strlen(str));
// sizeof(str) 返回的是数组占用的内存大小,而 strlen(str) 返回的是字符串的长度。
printf("%c-%s-%ld-%ld\n", *p, p, sizeof(p), strlen(p));
// sizeof(p) 返回的是int型指针占用的内存大小,而 strlen(p) 返回的是字符串的长度。
*p='A';
*(p+1)='B';
std::cout<<*str<<*(str+1)<<std::endl;
return 0;
}
2、此处会有一个warning,因为“ab”是存储在常量区域的,不能修改,在使用时需要使用const来申明
#include <iostream>
#include <cstring>
int main() {
char *p="ab";// 先在常量区保存好"ab",然后在栈区建立一个char *
// const char *p="ab";
printf("%c-%s-%ld-%ld\n", *p, p, sizeof(p), strlen(p));
// sizeof(p) 返回的是int型指针占用的内存大小,而 strlen(p) 返回的是字符串的长度。
*p='A';
*(p+1)='B';
std::cout<<*p<<*(p+1)<<std::endl;
return 0;
}
3、此处会显示error: assignment of read-only location ‘* p’
#include <iostream>
#include <cstring>
int main() {
const char *p="ab";
printf("%c-%s-%ld-%ld\n", *p, p, sizeof(p), strlen(p));
// sizeof(p) 返回的是int型指针占用的内存大小,而 strlen(p) 返回的是字符串的长度。
*p='A';
*(p+1)='B';
std::cout<<*p<<*(p+1)<<std::endl;
return 0;
}
4、字符数组指针的初始化
#include <iostream>
#include <cstring>
using namespace std;
int main() {
char a='a';
char b='b';
char *p=&a;
char c[]={a, b, 'c'};
printf("%p--%p--%c--%c\n", &a, &b, a, b);
printf("%p--%p--%c--%c\n", c, c+1, *c, *(c+1));
char *p2[]={&a, p, c};
for(int i=0; i<3; i++){
printf("%p--%p--%c\n", &p2[i], p2[i], *p2[i]);
}
return 0;
}
函数指针
主要要把指针括起来
#include <iostream>
using namespace std;
void func(){
cout<<"func"<<endl;
}
int add(int a, int b){
return a+b;
}
int main(){
int a=123;
int *p=&a;
printf("%p--%p\n", &a, p);
//无参
void (*pFunc)()=&func;//要不要&,都可
printf("%p--%p\n", func, pFunc);
//带参
int (*pFunc1)(int, int)=&add;
printf("%p--%p\n", add, pFunc1);
return 0;
}
函数指针数组
#include <iostream>
using namespace std;
void func(){
cout<<"func"<<endl;
}
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a - b;
}
int mul(int a, int b){
return a*b;
}
int div11(int a, int b){
return a/b;
}
int main(){
int a=1;
int b=2;
int *p=&a;
printf("%p--%p\n", &a, p);
int (*pFunc[4])(int, int)={add, sub, mul, div11};
for(int i=0; i<4; i++){
cout<<(*pFunc[i])(a, b)<<endl;
}
return 0;
}
函数指针深度辨析
#include <iostream>
using namespace std;
void func(){
cout<<"func"<<endl;
}
int add(int a, int b){
return a+b;
}
int main(){
void (*pFunc)()=&func;
printf("%p\n", &pFunc);
printf("%p\n", pFunc);
printf("%p\n", *pFunc);
printf("%p\n", **pFunc);
printf("%p\n", ***pFunc);
printf("----------\n");
printf("%p\n", &func);
printf("%p\n", func);
printf("%p\n", *func);
printf("%p\n", **func);
printf("%p\n", ***func);
return 0;
}
这段代码试图对函数指针的使用进行深度探索,并分析各种形式下的打印结果。然而,这里涉及到一些误用和未定义行为,下面是详细的解析。
1. 函数指针的基本概念
函数指针是一种特殊的指针,用于存储函数的入口地址。
定义函数指针的语法:
返回值类型 (*指针名)(参数列表);
示例:
void (*pFunc)(); // 定义一个返回值为void,参数为空的函数指针
2. 示例代码解析
代码片段:
void func() {
cout << "func" << endl;
}
int add(int a, int b) {
return a + b;
}
int main() {
void (*pFunc)() = &func;
printf("%p\n", &pFunc); // 打印函数指针变量的地址
printf("%p\n", pFunc); // 打印函数的入口地址
printf("%p\n", *pFunc); // 正确调用:实际上是调用了函数
printf("%p\n", **pFunc); // 错误:未定义行为
printf("%p\n", ***pFunc); // 错误:未定义行为
printf("----------\n");
printf("%p\n", &func); // 打印函数的入口地址(与 pFunc 相同)
printf("%p\n", func); // 打印函数的入口地址(与 &func 相同)
printf("%p\n", *func); // 错误:未定义行为
printf("%p\n", **func); // 错误:未定义行为
printf("%p\n", ***func); // 错误:未定义行为
return 0;
}
3. 逐行解析与结果
3.1 第一部分:pFunc
的打印
void (*pFunc)() = &func;
printf("%p\n", &pFunc); // 打印函数指针变量 pFunc 的地址
printf("%p\n", pFunc); // 打印函数的入口地址
printf("%p\n", *pFunc); // 函数调用,实际输出 "func"
printf("%p\n", **pFunc); // 错误:未定义行为
printf("%p\n", ***pFunc); // 错误:未定义行为
&pFunc
:打印的是指针变量pFunc
的地址(不是函数的地址)。pFunc
:打印的是函数func
的入口地址,和&func
相同。*pFunc
:
- 函数指针解引用后实际是调用该函数,即会输出
"func"
,但不能使用%p
打印返回值。 %p
是打印指针地址,而函数返回值类型是void
,这会导致未定义行为。
- 函数指针解引用后实际是调用该函数,即会输出
**pFunc
和***pFunc
:
- 解引用次数超出函数指针的定义范围,属于未定义行为。
3.2 第二部分:func
的打印
printf("%p\n", &func); // 打印函数的入口地址
printf("%p\n", func); // 打印函数的入口地址
printf("%p\n", *func); // 错误:未定义行为
printf("%p\n", **func); // 错误:未定义行为
printf("%p\n", ***func); // 错误:未定义行为
&func
和func
:对于函数名func
,它既表示函数入口地址,也是函数指针常量。*func
、**func
和***func
:
- 函数名不能被多次解引用,尝试这样操作会导致编译错误或未定义行为。
4. 函数指针的正确使用方式
4.1 使用函数指针调用函数
void func() {
cout << "This is func" << endl;
}
int main() {
void (*pFunc)() = func; // 函数指针指向函数
pFunc(); // 使用函数指针调用函数
(*pFunc)(); // 解引用后调用(等价于 pFunc())
return 0;
}
4.2 函数指针作为参数
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
// 函数指针作为参数
int calculate(int (*operation)(int, int), int x, int y) {
return operation(x, y);
}
int main() {
cout << calculate(add, 5, 3) << endl; // 输出 8
cout << calculate(subtract, 5, 3) << endl; // 输出 2
return 0;
}
4.3 函数指针作为返回值
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int (*getOperation(bool isAdd))(int, int) {
return isAdd ? add : subtract; // 返回函数指针
}
int main() {
int (*operation)(int, int) = getOperation(true); // 返回 add
cout << operation(5, 3) << endl; // 输出 8
operation = getOperation(false); // 返回 subtract
cout << operation(5, 3) << endl; // 输出 2
return 0;
}
5. 总结
5.1 函数指针的特性
- 函数指针存储函数入口地址,可用于动态调用。
- 解引用函数指针后可以调用函数,但额外多次解引用是未定义行为。
- 函数名本身可以隐式转换为函数指针。
5.2 打印函数指针
%p
适用于打印函数指针地址或函数入口地址。- 解引用函数指针后不能直接打印,解引用后应调用函数。
5.3 函数指针的用法
- 函数指针常用于回调机制、动态选择操作、简化代码结构。
- 小心未定义行为(如多次解引用、返回局部指针等)。
通过合理使用函数指针,可以提高代码的灵活性和可扩展性。
结构体内存对齐
#include <iostream>
using namespace std;
#define sz(type) cout<<sizeof(type)<<endl;
struct stu{
char c;
short a;
int *b;
};//8
struct stu1{
char c;
int *b;
short a;
};//12
struct stu2
{
double a;
char b;
int *c;
};//16
struct stu3
{
char b;
double a;
int *c;
};//24
int main(){
sz(stu);
sz(stu1);
sz(stu2);
sz(stu3);
return 0;
}
结构体指针
#include <iostream>
#include <cstring>
using namespace std;
struct Stu{
int a;
char c[13];
};
int main(){
Stu s;
s.a=12;
strcpy(s.c, "121");
cout<<sizeof(s)<<endl;
cout<<s.a<<"--"<<s.c<<endl;
Stu *pStu=(Stu *)malloc(sizeof(Stu));
pStu->a=11;
strcpy(pStu->c,"qwe");
cout<<pStu->a<<"---"<<pStu->c<<endl;
return 0;
}
这段代码演示了 结构体指针的基本用法,以及通过 malloc
动态分配内存来操作结构体的一个实例。以下是代码的分步解析:
1. 代码运行及输出
1.1 静态分配的结构体
Stu s;
s.a = 12;
strcpy(s.c, "121");
cout << sizeof(s) << endl;
cout << s.a << "--" << s.c << endl;
Stu
结构体内存布局(假设 64 位系统):int a -> 4 字节 char c[13] -> 13 字节 填充 -> 3 字节(以满足 4 字节对齐) 总大小 -> 20 字节
sizeof(s)
输出:20
(总大小包含对齐后的填充字节)。s.a
和s.c
的值:分别为12
和字符串"121"
。
1.2 动态分配的结构体
Stu *pStu = (Stu *)malloc(sizeof(Stu));
pStu->a = 11;
strcpy(pStu->c, "qwe");
cout << pStu->a << "---" << pStu->c << endl;
- 动态分配了大小为
sizeof(Stu)
的内存块。 - 使用箭头操作符 (
->
) 对结构体的成员进行赋值和访问。 - 输出:
11---qwe
。
2. 关键知识点
2.1 结构体的静态分配
- 定义
Stu s;
会在栈上分配内存,其生命周期由当前作用域决定。 - 通过点操作符 (
.
) 访问和操作结构体的成员。 - 对齐填充:
- 为了提高 CPU 访问效率,系统通常要求结构体成员对齐到其最大类型大小的倍数(此处为 4 字节)。
char c[13]
后需要填充 3 字节,确保结构体总大小为 4 的倍数。
2.2 结构体的动态分配
- 使用
malloc
分配内存时,内存是从堆上分配的,生命周期由程序员手动控制(需注意释放内存以防止内存泄漏)。 - 动态分配的结构体需要通过 指针访问,使用箭头操作符 (
->
) 访问成员。
2.3 内存管理注意事项
动态分配的内存需要释放:
free(pStu);
否则会造成内存泄漏。
避免使用未初始化的内存: 动态分配后,内存中的值是未定义的,需显式赋值或初始化。
Stu *pStu = (Stu *)malloc(sizeof(Stu)); memset(pStu, 0, sizeof(Stu)); // 初始化为 0
使用
malloc
时注意类型转换: 在 C++ 中,最好使用new
而不是malloc
:Stu *pStu = new Stu; delete pStu;
或者可以使用现代的智能指针来管理动态内存,避免手动释放的麻烦。
3. 输出结果(假设 64 位系统)
运行程序时会得到如下输出:
20
12--121
11---qwe
4. 推荐改进
4.1 替换 malloc
C++ 提供了更安全的内存分配方法,可以用 new
替换 malloc
:
Stu *pStu = new Stu;
pStu->a = 11;
strcpy(pStu->c, "qwe");
cout << pStu->a << "---" << pStu->c << endl;
delete pStu;
4.2 使用智能指针
使用 std::unique_ptr
或 std::shared_ptr
来管理动态分配的结构体:
#include <memory>
std::unique_ptr<Stu> pStu = std::make_unique<Stu>();
pStu->a = 11;
strcpy(pStu->c, "qwe");
cout << pStu->a << "---" << pStu->c << endl;
这样可以自动释放内存,无需手动调用 delete
或 free
。
5. 总结
- 动态分配的内存需手动管理,推荐使用现代 C++ 的
new/delete
或智能指针代替malloc/free
。 - 结构体的大小受对齐规则影响,可以通过调整成员顺序减少填充字节。
- 对动态分配的结构体指针,尽量初始化,避免出现未定义行为。