C++学习笔记之数组&指针

一、数组

(一)数组的概念

基本数据类型:C++提供了多种基本数据类型,如int、float、double、char等。这些数据类型用于存储不同类型的数据。
用户定义的数据类型:除了基本数据类型,用户还可以定义自己的数据类型,如结构体(struct)、类(class)等。
数组:数组是一种可以存储多个同类型数据的数据结构。例如,一个整数数组可以存储多个整数。
字符串:C++中,字符串通常表示为字符数组,也可以使用标准库中的std::string类来处理。

数组是一种数据结构,它包含一组有序的元素,每个元素可以通过其索引来访问。在数组中,每个元素都有一个唯一的索引,该索引用于标识该元素在数组中的位置。数组的大小是固定的,一旦创建,其大小就不能更改。

在许多编程语言中,如C++、Java和Python等,都有数组这种数据结构。数组可以是一维或多维的,其中一维数组是最简单的形式,而多维数组可以用来表示矩阵、三维空间中的点等复杂的数据结构。

二、指针

(一)指针的概念

定义:指针是一个变量,其值为另一个变量的地址。通过指针,我们可以间接访问和修改变量的值。
指针的声明:例如,int *ptr;声明了一个指向整数的指针ptr。

举一个简单的例子来说明指针的概念:

假设你有一本电话簿,上面列出了许多朋友的电话号码。每个电话号码占据了一个页码。如果你想给朋友打电话,你首先需要找到相应的页码,然后翻到那一页找到电话号码。在这个场景中,页码就是指针,它指向了电话号码的具体位置。

现在,如果我们想给不同的朋友打电话,我们可能需要多次翻动电话簿。这种频繁的翻动很麻烦,但如果我们可以直接找到所有朋友的电话号码的存放位置,一次性记录下来,那么我们就可以快速地找到任何一个朋友的电话号码。这个存放所有电话号码的位置就是指针数组。

通过这个例子,可以将指针理解为一个指南针,它指向了某个数据的位置,这样我们就可以快速地找到并使用这些数据。

在C++中,我们用星号(*)前缀来表示指针。例如,如果你声明了一个int类型的变量和一个指向int的指针,你可以这样做:

int num = 10;  // 定义一个int变量  
int *ptr;      // 定义一个指向int的指针

接下来,让指针指向这个变量:

ptr = #  // ptr现在存储了num的地址

&符号用于获取变量的地址。当我们对一个变量使用&操作符时,我们得到的是该变量的内存地址,而不是它的值。
现在,你可以通过指针来访问和修改变量的值:

*ptr = 20;  // 通过指针修改num的值

以上就是指针的基本概念。

再举一个简单的示例,演示指针和&符号的使用:

#include <iostream>  
  
int main() {  
    int num = 10;  
    int *ptr = &num;  // ptr指向num的地址  
  
    std::cout << "num的值:" << num << std::endl;  
    std::cout << "num的地址:" << &num << std::endl;  
    std::cout << "ptr指向的地址:" << ptr << std::endl;  
    std::cout << "ptr指向的值:" << *ptr << std::endl;  // 使用*来解引用指针,获取指针所指向的值  
  
    return 0;  
}

输出的结果为:

num的值:10
num的地址:0x61fe14
ptr指向的地址:0x61fe14
ptr指向的值:10

在这个示例中,我们定义了一个整数变量num,并声明了一个指向整数的指针ptr。通过使用&符号,我们将num的地址赋给ptr。然后,我们使用*符号来解引用指针,获取指针所指向的值,即num的值。

(二)指针的算数操作

指针的算术操作包括指针的加法、减法、算术赋值和比较操作。

  • 加法:将一个整数加到一个指针上,会按照该指针类型的大小移动指针。例如,如果一个指针是int *类型,那么ptr +1会将指针向前移动4个字节(在32位系统上),因为int类型通常占用4个字节。
  • 减法:将一个整数从一个指针中减去,同样会按照该指针类型的大小移动指针。例如,ptr2 - ptr1会计算两个指针之间的差值,结果是一个整数。
  • 算术赋值:指针的算术赋值操作与常规的算术赋值类似,例如ptr += 5等价于ptr =ptr + 5
  • 比较操作:可以使用比较操作符(如==, !=, <, >, <=, >=)来比较两个指针。比较的结果取决于指针所指向的数据类型和它们的相对位置。

请注意,当对指针进行算术操作时,必须确保指针指向有效的内存地址,否则可能导致未定义的行为或程序崩溃。此外,不同编译器和平台可能对指针算术有不同的行为,因此在实际应用中应谨慎处理。

(三)注意事项

1. 空指针

当一个指针被声明了,但没有被赋值时,它的值是未定义的,这就是所谓的空指针。空指针不同于NULL指针,NULL指针的值是0。

#include <iostream>

int main() {
    int *ptr;  // 声明一个整型指针

    if (ptr == nullptr) {
        std::cout << "ptr is null" << std::endl;
    } else {
        std::cout << "ptr is not null" << std::endl;
    }
    return 0;
}

在这个例子中,我们声明了一个整型指针ptr,但没有给它赋值。然后我们检查ptr是否为空。
因为ptr是未初始化的,所以它的值是未定义的,可能是任何值,包括0。
因此,上面的代码可能会输出"ptr is null",也可能会输出"ptr is not null",这取决于ptr的未定义值。

为了避免这种不确定性,最好在使用指针之前将其初始化为NULLnullptr。这样,你可以明确地知道指针是否为空,避免出现未定义的行为。

2. 野指针

野指针是指一个指针被删除或释放后,仍然保留着原来的内存地址,但由于该内存地址已经不属于任何有效的变量或对象,因此被称为“野指针”。

野指针是非常危险的,因为它们可能会导致程序崩溃、数据损坏或其他未定义的行为。当一个指针指向的内存被释放后,该指针就变成了野指针,如果仍然试图通过这个指针访问内存,就会产生不可预知的结果。

为了避免野指针的问题,应该遵循以下规则:

  • 初始化指针:在使用指针之前,应该将其初始化为NULLnullptr,以确保它不指向任何有效的内存地址。
  • 检查指针是否为空:在使用指针之前,应该检查它是否为空。如果指针为空,则不应该试图访问它所指向的内存。
  • 不要重复使用内存:一旦释放了指针所指向的内存,就应该将该指针设置为NULLnullptr,以便清楚地表明它不再指向任何有效的内存地址。
  • 使用智能指针:智能指针是一种现代C++特性,它可以自动管理内存的生命周期,从而避免内存泄漏和野指针的问题。智能指针有三种类型:unique_ptrshared_ptrweak_ptr

使用智能指针的示例:

#include <iostream>  
#include <memory> 

/*具体来说,<memory> 头文件提供了几种智能指针类型:
std::unique_ptr: 独占所有权的智能指针,确保所指向的对象在任何时刻都只有一个unique_ptr拥有它。当unique_ptr被销毁时,它所指向的对象也会被自动删除。
std::shared_ptr: 允许多个指针共享同一个对象的所有权。当最后一个指向对象的shared_ptr被销毁时,对象才会被删除。
std::weak_ptr: 与shared_ptr一起使用,用于解决shared_ptr之间的循环引用问题。
在这段代码中,std::make_unique 和 std::make_shared 是创建智能指针的便捷函数,它们也定义在 <memory> 头文件中。*/

//定义一个名为MyClass的类。
class MyClass {  
public:  
//类的访问修饰符,表示以下的内容是公共的,可以在类的外部被访问
    MyClass(int value) : value_(value) {}  
//MyClass的构造函数。它接受一个整数参数value,并使用这个值初始化成员变量value_。
    void printValue() { std::cout << value_ << std::endl; }  
//这是一个公共成员函数,名为printValue。它打印成员变量value_的值到标准输出。
private:  
//修饰符,表示以下的内容是私有的,只能在类的内部被访问
    int value_;  
};  
  
int main() {  
    // 使用std::make_unique创建unique_ptr  
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>(10);  
    ptr1->printValue();  // 输出:10  
  
    // 使用std::make_shared创建shared_ptr  
    std::shared_ptr<MyClass> ptr2 = std::make_shared<MyClass>(20);  
    ptr2->printValue();  // 输出:20  
  
    return 0;  
}

在这个示例中,我们使用了两种智能指针:std::unique_ptrstd::shared_ptr
std::unique_ptr是一种独占所有权的智能指针,它确保了所指向的对象在任何时刻都只有一个unique_ptr拥有它。
unique_ptr被销毁时(例如,离开其作用域),它所指向的对象也会被自动删除。而std::shared_ptr则允许多个指针共享同一个对象的所有权。当最后一个指向对象的shared_ptr被销毁时,对象才会被删除。

3. 类型匹配

在C++中,指针的类型匹配非常重要,因为它决定了指针所指向的数据类型以及如何通过指针访问这些数据。例如,一个int *类型的指针不能用来存储double类型的地址。

以下是关于指针类型匹配的一些要点:

  • 定义指针类型:在定义指针时,必须指定指针所指向的数据类型。例如,int *ptr; 定义了一个指向整数的指针。
  • 类型转换:如果一个指针被定义为指向一种数据类型,则不能将其用于指向其他类型的对象。但是,可以通过类型转换来改变指针的类型。例如以下代码,将 ptr2 转换为指向整数的指针。
 int *ptr = static_cast<int *>(ptr2); 
  • 兼容性:在大多数情况下,如果一个指针的类型与所指向对象的类型不兼容,编译器会发出警告或错误。例如,如果一个指针被定义为指向整数,但被用于指向浮点数,编译器通常会发出警告或错误。
  • 强制类型转换:可以使用强制类型转换来将一个指针转换为另一种类型(应该谨慎,因为可能导致未定义的行为)。例如,
int *int_ptr = (int *) ptr; 
  • 多态性:在面向对象编程中,可以通过基类指针或引用访问派生类对象,这称为多态性。在这种情况下,需要使用虚函数和动态绑定来实现多态性。

4. 内存管理

用new分配的内存必须使用delete释放,使用malloc分配的内存必须使用free释放。
忘记释放内存会导致内存泄漏。

  • 使用new分配的内存必须使用delete释放:

在C++中,new操作符用于在堆上动态分配内存。当你使用new为一个对象分配内存时,你实际上是在请求内存分配器从堆上为你取出一块足够大的内存。当你不再需要这块内存时,应该使用delete操作符来释放它。如果你忘记使用delete,就会发生内存泄漏,因为那块内存将永远不会被回收,从而导致程序占用的内存逐渐增加。

  • 使用malloc分配的内存必须使用free释放:

在C语言中(虽然C++也支持malloc和free),malloc和free是用于动态内存分配和释放的函数。与C++的new和delete类似,当使用malloc为一个数据结构分配内存时,应该使用free来释放它。忘记使用free同样会导致内存泄漏。

忘记释放内存会导致内存泄漏:——这是上述两点的一个总结。

如果你使用new、malloc或其他类似的函数为数据结构分配了动态内存,但之后没有释放它,那么这块内存将永远不会被回收。
这不仅会导致程序使用的内存量逐渐增加,而且如果程序长时间运行或频繁进行动态内存分配,可能会导致可用内存耗尽,从而引发严重的问题。
为了避免这些问题,通常建议程序员使用智能指针(如C++11引入的std::unique_ptrstd::shared_ptr)来管理动态内存。智能指针可以自动管理对象的生命周期,确保在指针超出作用域时内存被正确释放,从而减少因忘记释放内存而导致的错误。

#include <iostream>  
  
class MyClass {  
public:  
    MyClass() { std::cout << "MyClass constructor called." << std::endl; }  
    ~MyClass() { std::cout << "MyClass destructor called." << std::endl; }  
};  
  
int main() {  
    // 使用new在堆上动态分配内存  
    MyClass* ptr = new MyClass();  
      
    // 在此处执行其他操作...  
      
    // 使用delete释放内存  
    delete ptr;  
      
    return 0;  
}

三、数组与指针

在C++中,数组名本质上是指向数组第一个元素的常量指针。这句话可以这样理解:

在C和C++中,数组名实际上是一个指向数组第一个元素的常量指针(或者说是地址)。当我们定义一个数组,如int arr[10];,这个数组的名字(arr)就代表了数组的第一个元素的地址。

例如,如果我们想访问数组的第一个元素,我们可以使用数组名直接访问:

int arr[10];  
arr[0] = 5;  // 这等价于 *(arr + 0) = 5;

在上面的代码中,arr实际上是一个指向数组第一个元素的指针。当我们写arr[0],这等价于*(arr + 0)。在C和C++中,数组名会自动进行这个指针算术操作,所以我们通常直接使用数组名来访问元素。

需要注意的是,数组名是一个常量指针,所以你不能改变它所指向的地址。也就是说,你不能让数组名指向其他地方。例如,以下的代码是错误的:

int arr[10];  
int *p = arr;  // 错误!你不能改变数组名的指向。

总结一下,数组名本质上是指向数组第一个元素的常量指针。它为我们提供了一种方便的方式来访问数组元素。

注意函数参数:当传递数组到函数时,实际上是传递了数组的首地址。如果你想在函数内部修改数组的内容,你需要传递指向数组的指针。

const关键字:当你声明一个指针为const时,意味着你不能通过这个指针修改所指向的值。例如,const int *ptr;表示你不能通过ptr来修改变量的值。

  • 11
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值