C++指针复习


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 动态内存分配与指针

通过newdelete动态分配内存:

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. 常见问题与调试技巧

  1. 空指针解引用

    int* p = nullptr;
    std::cout << *p << std::endl; // segmentation fault
  2. 指针越界

    int arr[3] = {1, 2, 3};
    int* p = arr + 3; // 超出数组范围
    std::cout << *p << std::endl; // 未定义行为
  3. 野指针

    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修饰指针的两种情况

  1. 指向常量的指针(const修饰指向内容)

    • 指针可以指向不同地址,但指向的内容不可修改。
    const int *p = &a; // 或 int const *p = &a;
    p = &b;           // OK: 修改指针地址
    *p = 10;          // 错误: 修改指向内容
  2. 指针常量(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. &aa 的区别

  • a 是指向首元素的指针,等同于 &a[0]
  • &a 是整个数组的地址,表示一个数组整体。

两者类型不同:

  • a 的类型是 int*,步长是单个元素的大小(sizeof(int))。
  • &a 的类型是 int(*)[3],步长是整个数组的大小(sizeof(a))。

4. 地址运算

  1. a + i:表示第 i 个元素的地址。
  2. &a + i:表示第 i 个数组的地址,步长是数组大小(即 3 × sizeof(int))。
  3. &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大小)

总结

  1. a&a 的区别
    • a 是指向首元素的指针,类型是 int*,步长是单个元素的大小。
    • &a 是指向整个数组的指针,类型是 int(*)[3],步长是整个数组的大小。
  2. sizeof 的结果
    • sizeof(a) 是数组整体大小,等于 数组长度 × 单元素大小
    • sizeof(&a) 是指针大小,通常是 4 或 8 字节。
  3. 地址运算步长
    • 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[行号][列号] 表示元素地址

总结

  1. 二维数组的数组名a 是指向第0行的指针,类型为 int (*)[列数],步长是单行大小。
  2. 行与元素的指针:
    • a[i] 是指向第 i 行首元素的指针,类型为 int*
    • &a[i] 是第 i 行整体的地址,类型为 int (*)[列数]
  3. 地址运算步长:
    • 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++ 中,数组名作为函数参数传递时,会退化为指针。以下是具体表现:

数组名的特性

  1. 数组名本质:数组名是数组首元素的地址,但它保留了数组的整体大小信息。
  2. 传递到函数中:当数组名作为参数时,它退化为普通指针,数组大小信息丢失。

示例代码

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

总结

  1. 宏定义:代码简单,但缺乏类型安全,不推荐。
  2. 指针传递:显式操作地址,适合 C 风格代码。
  3. 引用传递:代码更现代化,推荐使用。
  4. 模板:支持泛型交换,适用于复杂类型,是最通用的方式。

通过这些实现方式,可以灵活选择适合需求的方案,同时理解数组在函数参数中退化为指针的行为,有助于写出更清晰、可靠的代码。

指针作为返回值

要作为返回值,必须保证调用后返回的指针不会销毁,否则就是野指针。

#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 全局数组的特点

  1. 全局变量的生命周期
    • 全局变量的生命周期贯穿整个程序运行周期,直到程序结束才会被销毁。
    • 因此返回全局变量的地址是安全的,不会出现野指针。
  2. 全局数组的初始化
    • 如果没有显式赋值,全局数组中的元素会被初始化为 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. 返回值类型分析
    • 全局变量:安全,但可能引入全局状态问题。
    • 局部变量:危险,容易导致野指针。
    • 动态分配内存:安全,但需手动管理内存。
    • 静态变量:安全,但需注意多线程安全问题。
  2. 建议
    • 尽量避免使用全局变量。
    • 优先考虑返回动态分配的内存或局部静态变量。
    • 在多线程环境下,需注意同步访问问题。

通过理解指针生命周期和作用域,能更好地写出健壮和高效的代码。

字符数组指针

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); // 错误:未定义行为
  • &funcfunc:对于函数名 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 函数指针的特性

  1. 函数指针存储函数入口地址,可用于动态调用。
  2. 解引用函数指针后可以调用函数,但额外多次解引用是未定义行为。
  3. 函数名本身可以隐式转换为函数指针。

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.as.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 内存管理注意事项

  1. 动态分配的内存需要释放

    free(pStu);

    否则会造成内存泄漏。

  2. 避免使用未初始化的内存: 动态分配后,内存中的值是未定义的,需显式赋值或初始化。

    Stu *pStu = (Stu *)malloc(sizeof(Stu));
    memset(pStu, 0, sizeof(Stu));  // 初始化为 0
  3. 使用 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_ptrstd::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;

这样可以自动释放内存,无需手动调用 deletefree


5. 总结

  1. 动态分配的内存需手动管理,推荐使用现代 C++ 的 new/delete 或智能指针代替 malloc/free
  2. 结构体的大小受对齐规则影响,可以通过调整成员顺序减少填充字节。
  3. 对动态分配的结构体指针,尽量初始化,避免出现未定义行为。

文章作者: AllenMirac
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 AllenMirac !
  目录