Skip to content

指针

本质上说,指针和普通变量没有区别。

指针都是存储在栈上或堆上,不管在栈上还是堆上,都一定有一个地址。

因为不同的变量类型占用不同的存储空间, 所以声明指针变量必须指定指针所指向变量的数据类型

在32位系统中,int 变量和指针都是32位。指针必须和&*这两个符号一起使用才有意义。

&aa*a
a这个变量的地址a对应地址存储的值对应地址存储的值作为地址对应的值

NULL和nullptr

类型安全: nullptr 是C++11引入的关键字,它是一个指针类型,明确表示空指针。 NULL 是一个宏定义,通常被定义为整数0或者(void*)0。在C++中,NULL 可能会被解释为整数0,这可能导致一些类型安全问题。 代码可读性:

使用 nullptr 可以更清晰地表达你的意图,表明你正在使用一个空指针。 使用 NULL 时,可能会让人误以为是一个整数0,降低代码的可读性。 函数重载: 在函数重载时,nullptr 可以更好地匹配指针类型的参数,避免歧义。 NULL 可能会被解释为整数0,导致重载决策错误。

总结:

  • 在C++中,推荐使用 nullptr 来表示空指针,因为它更安全、更具可读性,并且可以避免函数重载时的歧义。
  • 尽量避免使用 NULL,特别是在C++代码中。

数组

数组名是该数组首元素的地址 作为实际参数的数组名要求形式参数是一个与之匹配的指针

只有在这种情况下 c才会把int ar[]和int* ar解释成一样 也就是说 ar是指向int的指针 alt text

初始化

字符数组所有未被使用的元素都被自动初始化为'\0'

c
//m1和m2不一样 因为m2 最后缺少空字符 \0
const char m1[40] = "helloworld";
const char m2[40] = "'H','e','l'....";

//让编译器确定初始化字符数组的大小很合理 
//如果创建一个稍后再确定的数组就必须在声明时指定大小
const char m3[] = "if you";

储存位置

  • 通常字符串都作为可执行文件的一部分 储存在数据段中
  • 当把程序载入内存时 也载入了程序中的字符串(储存在静态储存区)。
  • 程序运行时 才会为该数组分配内存 此时将字符串拷贝到数组中。
  • 此时字符串副本有2个 一个是静态内存区 一个是array中的。

定义数组

  • *p允许++p, p++这样的操作。array[]不允许
  • *p通常只能用于变量前,不能用于常量。
  • array[]是地址常量
  • *p 该变量是最初指向该字符串的首字符
  • 如果打算修改字符串 就不要用指针指向字符串字面量

智能指针

直接使用new和delete运算符极其容易导致内存泄露,而且非常难以避免。

于是人们发明了智能指针这种可以自动回收内存的工具。

智能指针和裸指针不要混用,这一点非常重要

共享型智能指针 (shared_ptr)

普通的指针可以单独一个指针占用一块内存,也可以多个指针共享一块内存。同一块堆内存可以被多个shared_ptr共享。

在现代程序中,当想要共享一块堆内存时,优先使用shared_ptr,可以极大的减少内存泄露的问题。

工作原理

我们在动态分配内存时,堆上的内存必须通过栈上的内存来寻址。也就是说栈上的指针(堆上的指针也可以指向堆内存,但终究是要通过栈来寻址的)是寻找堆内存的唯一方式。

所以我们可以给堆内存添加一个引用计数,有几个指针指向它,它的引用计数就是几,当引用计数为0是,操作系统会自动释放这块堆内存。

常用操作

智能指针可以像普通指针那样使用,shared_ptr早已对各种操作进行了重载,就当它是普通指针就可以了。

初始化

一般来说不推荐使用new进行初始化,因为C++标准提供了专门创建shared_ptr的函数make_shared,该函数是经过优化的,效率更高。

cpp
std::shared_ptr<int> sharedI(new int(100));

使用make_shared函数进行初始化:

cpp
std::shared_ptr<int> sharedI = std::make_shared<int>(100);

注意:千万不要用裸指针初始化shared_ptr,容易出现内存泄露的问题。

cpp
//虽然可以这样做 但是极容易导致内存泄漏问题
int* pi = new int(100);
std::shared_ptr<int> sharedI(pi);

当然使用复制构造函数初始化也是没有问题的。

cpp
//如果不能用 那么共享型的共享二字还有什么意义呢 所以这是天经地义的
std::shared_ptr<int> sharedI1 = std::make_shared<int>(100);
std::shared_ptr<int> sharedI2(sharedI1);

引用计数

智能指针就是通过引用计数来判断释放堆内存时机的。use_count()函数可以得到shared_ptr对象的引用计数。

cpp
int main() {
    std::shared_ptr<int> sharedI = std::make_shared<int>(100);
    std::cout << sharedI.use_count() <<std::endl;

    std::shared_ptr<int> sharedI2(sharedI);
    std::cout << sharedI.use_count() <<std::endl;

    shared2.reset();
    std::cout << sharedI.use_count() << std::endl;
}

常用函数

unique函数

判断该shared_ptr对象是否独占若独占,返回true。否则返回false。

reset函数

  • reset函数有参数时,改变此shared_ptr对象指向的内存。
  • reset函数无参数时,将此shared_ptr对象置空,也就是将对象内存的指针设置为nullptr

get函数

  • 智能指针就是对裸指针的封装。这个函数就是获取裸指针。
  • 强烈不推荐使用。因为太不推荐使用, 所以visual studio的列表中都没有。
  • 如果一定要使用,那么一定不能delete返回的指针。

swap函数

  • 交换两个智能指针所指向的内存
  • std命名空间中全局的swap函数
  • shared_ptr类提供的swap函数

注意事项

创建数组

cpp
int main() {
    //有这个语法 但是c++20后才可以放心这么做
    std::shared_ptr<int> sharedI(new int[100]());
    std::cout <<  sharedI.get()[10] << std::endl;
}

参数传递

用智能指针作为参数传递时直接值传递就可以了。

  • shared_ptr的大小为固定的8或16字节
  • 也就是两倍指针的的大小,32位系统指针为4个字节,64位系统指针为8个字节,shared_ptr中就两个指针
  • shared_ptr就是封装了一个指针,一个计数器。真正的内容都在堆空间中。所以直接值传递就可以了。
cpp
void myFunc(std::shared_ptr<int> sharedI) {}

弱引用智能指针 (weak_ptr)

也是一种共享型智能指针,可以视为对共享型智能指针的一种补充

这个智能指针是在C++11的时候引入的标准库,它的出现完全是为了弥补shared_ptr天生有缺陷的问题

其实shared_ptr可以说近乎完美。只是通过引用计数实现的方式也引来了引用成环的问题,这种问题靠它自己是没办法解决的。

所以在C++11的时候将shared_ptrweak_ptr一起引入了标准库,用来解决循环引用的问题。

以下这段代码出现了循环引用的问题

cpp
class B;
class A{
public:
    std::shared_ptr<B> sharedB;
};

class B{
public:
    std::shared_ptr<A> sharedA;
};

int main() {
    std::shared_ptr<A> sharedA = std::make_shared<A>();
    std::shared_ptr<B> sharedB = std::make_shared<B>();
    //2个堆内存 你指我 我指你 都在等对方相互释放 就内存泄漏了
    shardA->shardB = shardB;
    shardB->shardA = shardA;
}

我们可以将其中一个改成weak_ptr来解决循环引用的问题

cpp
class B;
class A{
public:
    std::weak_ptr<B> weakB;
};

class B{
public:
    std::shared_ptr<A> sharedA;
};

int main() {
    std::shared_ptr<A> sharedA = std::make_shared<A>();
    std::shared_ptr<B> sharedB = std::make_shared<B>();
    shardA->weakB = shardB;
    shardB->shardA = shardA;
}

作用原理:

weak_ptr的对象需要绑定到shared_ptr对象上,作用原理是weak_ptr不会改变shared_ptr对象的引用计数。

只要shared_ptr对象的引用计数为0,就会释放内存,weak_ptr的对象不会影响释放内存的过程。

weak_ptr使用较少,就是为了处理shared_ptr循环引用问题而设计的。

独享型智能指针 (unique_ptr)

  • 同一块堆内存只能被一个unique_ptr拥有。
  • 在使用智能指针时,我们一般优先考虑独占式智能指针,因为消耗更小。
  • 如果发现内存需要共享,那么再去使用shared_ptr

初始化

使用new运算符进行初始化

cpp
std::unique_ptr<int> uniqueI(new int(100));

使用make_unique函数进行初始化

cpp
std::unique_ptr<int> uniqueI = std::make_unique<int>(100);

常用操作

  • unque_ptr禁止复制构造函数,也禁止赋值运算符的重载。否则独占便毫无意义。
  • unqiue_ptr允许移动构造,移动赋值。移动语义代表之前的对象已经失去了意义,移动操作自然不影响独占的特性。
cpp
std::unique_ptr<int> uniqueI = std::make_unique<int>(100);
std::unique_ptr<int> uniqueI2(std::move(uniqueI));
cpp
std::unique_ptr<int> uniqueI = std::make_unique<int>(100);
std::unique_ptr<int> uniqueI2 = std::make_unique<int>(200);
uniqueI2 = std::move(uniqueI);
  • unique_ptr的对象为一个右值时,就可以将该对象转化为shared_ptr的对象。
cpp
void myFunc(std::unique_ptr<int> uniquePtr) {
    //但是要保证uniquePtr不能再单独使用了
    std::shared_ptr<int> sharedI(std::move(uniquePtr));
}

这个使用的并不多,需要将独占式指针转化为共享式指针常常是因为先前设计失误。注意:shared_ptr对象无法转化为unique_ptr对象。

常用函数

reset函数

  • 不带参数的情况下:释放智能指针的对象,并将智能指针置空。
  • 带参数的情况下:释放智能指针的对象,并将智能指针指向新的对象。

使用范围

能使用智能指针就尽量使用智能指针,那么哪些情况属于不能使用智能指针的情况呢?

有些函数必须使用C语言的指针,这些函数又没有替代,这种情况下,才使用普通的指针。

必须使用C语言指针的情况包括:

  • 网络传输函数,比如windows下的send,recv函数,只能使用c语言指针,无法替代.
  • c语言的文件操作部分。这方面C++已经有了替代品,C++的文件操作完全支持智能指针,所以在做大型项目时,推荐使用C++的文件操作功能(IO库详细讲解)。

除了以上两种情况,剩下的均推荐使用智能指针。

我们应该使用哪个智能指针呢?

  • 优先使用unique_ptr,内存需要共享时再使用shared_ptr
  • 当使用shared_ptr时,如果出现了循环引用的情况,再去考虑使用weak_ptr