C++ 中的指针、指针对象、普通对象、引用类型等概念理解

1. 指针

C++ 中的指针是一个非常重要的概念,它允许你直接访问内存中的地址,可以用来操作数据、实现数据结构,以及进行动态内存分配。下面是一些关于 C++ 指针的基本理解和用法:

指针的定义

指针是一个变量,其值是一个内存地址。你可以通过在变量名前面加上星号 * 来定义指针。例如:

1
2
int *ptr; // 定义一个整数指针
double *ptr2; // 定义一个双精度浮点数指针

取地址操作符

你可以使用取地址操作符 & 来获取一个变量的地址,将其存储在指针中。例如:

1
2
int x = 42;
int *ptr = &x; // ptr 指向变量 x 的地址

访问指针指向的值

你可以使用解引用操作符 * 来访问指针指向的内存位置的值。例如:

1
int y = *ptr; // y 的值现在是 x 的值,也就是 42

指针的空值

指针可以有一个空值,表示它不指向任何有效的内存位置。可以使用 nullptr 或 NULL(在旧的 C++ 标准中)来将指针设置为空值。

1
int *ptr = nullptr; // 或 int *ptr = NULL;

指针的算术运算

指针可以进行算术运算,如递增、递减和加法。这通常用于遍历数组或访问连续内存块。

1
2
3
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 指向数组的第一个元素
int element = *(ptr + 2); // 获取第三个元素的值,也就是 3

指针与数组

数组名本质上是一个指向数组第一个元素的指针。你可以使用指针来遍历数组元素。

1
2
int arr[3] = {10, 20, 30};
int *ptr = arr; // ptr 指向 arr 的第一个元素

指针与函数

指针还可以用于传递函数参数,允许函数修改调用者提供的数据。

1
2
3
4
5
6
7
void modifyValue(int *ptr) 
{
*ptr = 100;
}

int x = 10;
modifyValue(&x); // 修改 x 的值

动态内存分配

使用 new 运算符可以在堆上分配内存,并返回指向分配内存的指针。使用 delete 运算符释放这些内存。

1
2
3
int *ptr = new int; // 分配一个整数大小的内存块
*ptr = 42;
delete ptr; // 释放内存

2. 普通对象和指针对象

C++ 中的普通对象和指针对象有明显的区别,这些区别主要涉及到它们的声明、内存分配、访问方式和生命周期等方面。

声明方式:

  • 普通对象:普通对象的声明方式不包含指针符号 *,直接声明对象的类型和名称。
  • 指针对象:指针对象的声明需要在类型前面加上 * 符号,以表明它是一个指向某种类型的指针。
    1
    2
    int x; // 普通整数对象
    int *ptr; // 整数指针对象

内存分配:

  • 普通对象:普通对象通常在栈上分配内存。它们的生命周期受限于包含它们的作用域。
  • 指针对象:指针对象可以指向栈上的对象或堆上分配的对象。通过 new 运算符可以在堆上分配内存来存储对象。
    1
    2
    int y; // 普通整数对象,栈上分配
    int *ptr = new int; // 整数指针对象,指向堆上分配的整数

访问方式:

  • 普通对象:可以直接使用对象的名称访问其值。
  • 指针对象:需要使用解引用操作符 * 来访问指针对象指向的值。
    1
    2
    int value = y; // 直接访问 y 的值
    int value2 = *ptr; // 使用指针解引用访问指向的整数的值

生命周期:

  • 普通对象:普通对象的生命周期通常与其包含的作用域相同。当离开作用域时,它们将自动被销毁。
  • 指针对象:指针对象的生命周期通常需要手动管理。如果它们指向堆上分配的内存,你需要使用 delete 来释放内存,以避免内存泄漏。
    1
    delete ptr; // 释放指针对象 ptr 所指向的堆上内存

示例

假设你有一个名为 Person 的 C++ 类,该类包含姓名和年龄属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <string>

class Person
{
public:
std::string name;
int age;

// 构造函数
Person(std::string n, int a) : name(n), age(a) {}

// 成员函数
void introduce()
{
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
};

创建普通对象:

1
2
3
4
5
6
7
8
9
10
int main() 
{
// 创建普通对象
Person person1("Alice", 30);

// 访问对象的属性和调用成员函数
person1.introduce();

return 0;
}

在上述示例中,我们直接创建了一个名为 person1 的普通对象,然后访问了其属性和成员函数。

创建指针对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
int main() 
{
// 创建指针对象
Person* personPtr = new Person("Bob", 25);

// 通过指针访问对象的属性和调用成员函数
personPtr->introduce();

// 释放内存
delete personPtr;

return 0;
}

在上述示例中,我们使用 new 运算符创建了一个指向 Person 类的对象的指针 personPtr,然后使用箭头操作符 -> 访问对象的属性和成员函数。最后,我们使用 delete 运算符释放了动态分配的内存,以避免内存泄漏。

C++ 中的 .(点号)和 ->(箭头)

在C++中,.(点号)和 ->(箭头)是用于访问对象成员的两个不同操作符,它们的使用方式和含义有一些重要区别,取决于对象的类型。

点号 .:

  • 用于直接访问普通对象的成员,包括类的数据成员和成员函数。
  • 通常用于栈上或静态存储的对象,以及引用对象的成员。
    1
    2
    3
    SomeClass obj; // 创建 SomeClass 类的对象
    obj.memberVar = 42; // 访问对象的数据成员
    obj.memberFunction(); // 调用对象的成员函数

箭头 ->:

  • 用于访问指针对象所指向的对象的成员,包括数据成员和成员函数。
  • 通常用于堆上或动态分配的对象,或者当你有一个指向对象的指针时。
    1
    2
    3
    SomeClass *ptr = new SomeClass(); // 创建 SomeClass 类的对象指针
    ptr->memberVar = 42; // 访问指针所指向对象的数据成员
    ptr->memberFunction(); // 调用指针所指向对象的成员函数

总结:

  • 点号 . 用于直接访问对象的成员,通常用于普通对象。
  • 箭头 -> 用于通过指针访问对象的成员,通常用于指针对象或对象的引用。

3. 引用类型

声明方式:

  • 引用类型使用 & 符号来声明,它是一个别名,引用一个已存在的变量或对象。
  • 引用必须在声明时初始化,一旦引用了某个变量或对象,它将一直引用该变量或对象,不能改变引用的目标。
    1
    2
    int x = 42;
    int &ref = x; // 引用 x

指向:

  • 引用是原变量的别名,不会分配新的内存。
  • 引用只能引用已存在的变量或对象。

4. C++ 和 C# 对比

  • 在 C# 中的引用类型相当于 C++ 中的指针类型,尽管在语法和实现上有一些不同,但它们都用于引用对象而不是直接包含对象的数据。
  • 在 C++ 中的普通对象,与 C# 中的引用类型(Reference Type)并没有一个直接的等价物。
  • 然而,如果要寻找一个更接近的概念,C++ 中的普通对象可能与 C# 中的值类型(Value Type)有一些相似之处。在 C# 中,值类型包括结构 (struct) 和基本数据类型(如 int、double),它们在栈上分配内存,不受垃圾回收器管理,与 C++ 中的普通对象在内存分配和生命周期方面有些相似。

5. 栈内存和堆内存

分配方式:

  • 栈内存:栈内存是一种静态内存分配,用于存储函数调用的局部变量、函数参数和函数调用的返回地址。它的分配和释放是自动的,随着函数的进入和退出而发生。
  • 堆内存:堆内存是一种动态内存分配,用于存储程序中动态分配的数据,如通过 new、malloc 或类似的分配函数分配的对象。堆内存的分配和释放需要显式管理,否则可能导致内存泄漏。

速度:

  • 栈内存:栈内存的分配和释放速度通常比较快,因为它是通过简单的移动栈指针来实现的。
  • 堆内存:堆内存的分配和释放速度可能较慢,因为它需要更复杂的内存管理,包括查找合适的内存块,分配和释放。

大小限制:

  • 栈内存:栈内存的大小通常有限,因为它的空间由系统自动管理,同时还受到递归函数调用的限制。通常,栈的大小在几兆字节到几百兆字节之间。
  • 堆内存:堆内存的大小通常较大,受系统内存的总大小限制。在大多数现代系统中,堆可以非常大。

生命周期:

  • 栈内存:栈内存中的数据的生命周期与其包含的作用域相对应,当作用域结束时,栈上的数据会自动销毁。
  • 堆内存:堆内存中的数据的生命周期需要手动管理。如果不释放堆上分配的内存,可能会导致内存泄漏。

访问:

  • 栈内存:栈上的数据访问速度较快,因为它们通常在内存中紧密排列,利于缓存。
  • 堆内存:堆上的数据访问速度较慢,因为它们分散在内存中,需要通过指针来访问。

适用情况:

  • 栈内存:适用于存储生命周期较短的局部变量和函数参数。它通常用于维护程序的执行状态和函数调用。
  • 堆内存:适用于存储动态分配的数据,如复杂数据结构、对象、数组等,其生命周期可能跨越多个函数调用。