C++的指针和引用

简介: ● 内存由很多的内存单元组成,这些内存单元用于存放各种类型数据;● 计算机对内存的每个内存单元都进行了编号,这个编号就称为内存地址,地址决定了内存单元在内存中的位置;● 记住这些内存单元地址不方便,因此C++语言的编译器让我们通过名字来访问这些内存位置;

C++的指针和引用


C++指针


C++中内存单元内容和地址

●  内存由很多的内存单元组成,这些内存单元用于存放各种类型数据;

●  计算机对内存的每个内存单元都进行了编号,这个编号就称为内存地址,地址决定了内存单元在内存中的位置;

●  记住这些内存单元地址不方便,因此C++语言的编译器让我们通过名字来访问这些内存位置;

int a = 112, b = -1;
float c = 3.14;
int *d = &a;
float *e = &c;

指针的定义和间接访问操作

●  指针定义的基本形式:指针本身就是一个变量,其符合变量定义的基本形式,它存储的是值的地址。对类型T,T*是"到T的指针"类型,一个类型为T*的变量能保存一个类型T的对象的地址.


●  通过指针访问它所指向的地址的过程称为间接访问或者引用指针;


这个用于执行间接访问操作符是单目操作符*; cout << *d << endl;

#include <iostream>
using namespace std;
int main()
{
  int a = 112, b = -1;
  float c = 3.14f;
  int*  d = &a;
  float*  e = &c;
  cout << d << endl;    cout << e << endl;
  cout << (*d) << endl;    cout << (*e) << endl;
  return 0;
}

关于变量、地址和指针变量


●  一个变量的三个重要信息:

  • 变量地址信息
  • 变量所存的信息
  • 变量类型

●  指针变量是一个专门用来记录变量的地址的变量;通过指针变量可以间接的访问另一个变量的值。


指针和数组

如过是数组,数组地址是不可改变的,但是数组值是可以改变的


指针地址是可变的,指针值是否可改变取决于所指的区间的存储区域是否可变

#include <iostream>
using namespace std;
int main()                              
{
  // T*:  注意*在定义和间接访问上的作用
  //int i = 4;  int* iP = &i;   cout << (*iP) << endl;
  //double d = 3.14; double* dP = &d; cout << (*dP) << endl;
  //char c = 'a';  char* cP = &c; cout << (*cP) << endl;
  // array of pointers和a pointer to an array
  int c[4] = { 0x80000000, 0xFFFFFFFF, 0x00000000, 0x7FFFFFFF };
  int*  a[4];                          // array of pointers       指针的数组
  int(*b)[4];                         // a pointer to an array 数组的指针
  b = &c;                            // 注意:这里数组个数得匹配
  // 将数组c中元素赋给数组a
  for (unsigned int i = 0; i<4; i++)
  {
    a[i] = &(c[i]);
  }
  // 输出看下结果
  cout << *(a[0]) << endl;   // -2147483648
  cout << (*b)[3] << endl;   // 2147483647
  return 0;                            
}

左值和右值

●  概念:


一般说法,编译器为其单独分配了一块存储空间,可以取其地址的,左值可以放在赋值运算符左边;


右值指的是数据本身,不能取到自身地址,右值只能放在赋值运算符右边。


●  具体分析:


左值最常见的情况如函数和数据成员的名字;


右值是没有标识符、不可取地址的表达式,一般称之为临时对象。


几种C++中的原始指针

    ●  一般类型指针T*;


T是一个泛型,泛指任何一种类型


    ●  指针的数组与数组的指针


    指针的数组 T *t[] 数组的指针 T(*t)[]


    ●  const pointer和pointer to const


○  关于const修饰的部分

     ●  看左侧最近的部分

     ●  如果左侧没有,则看右侧

#include <iostream>
using namespace std;
unsigned int MAX_LEN = 11;
int main()
{
  char strHelloworld[] = { "helloworld" };
  char const* pStr1 = "helloworld";             // const char*
  char* const pStr2 = strHelloworld;          
  char const* const pStr3 = "helloworld";  // const char* const
  pStr1 = strHelloworld;
  //pStr2 = strHelloworld;                            // pStr2不可改
  //pStr3 = strHelloworld;                            // pStr3不可改
  unsigned int len = strnlen_s(pStr2, MAX_LEN);
  cout << len << endl;
  for (unsigned int index = 0; index < len; ++index)
  {
    //pStr1[index] += 1;                               // pStr1里的值不可改
    pStr2[index] += 1;
    //pStr3[index] += 1;                               // pStr3里的值不可改
  }
  return 0;
}

●  指向指针的指针


例子:


int a = 123;


int *b = &a;


int **c = &b;


   ○  *操作符具有从右向左的结合性

   ○  **这个表达式相当于*(*c) , 必须从里向外逐层求值

   ○  *c得到的是b的地址 **c相当于*b,得到a的值

●  未初始化和非法指针(野指针)


例子:int *a; // a的指向不明,可能会导致程序崩溃。


*a = 12;


最坏的情况是定位到了一个可访问的地址,无意间修改了它,这样的错误难以捕捉,引发的错误可能与原先用于操作的代码完全不相干。


●  NULL指针


一个特殊的指针变量,表示不指向任何东西 int *a = NULL;


 ○ NULL指针表示指针未指向任何东西

 ○ 对于一个指针,如果已经知道将被初始化的地址,那么请赋值给它,否则请将它设置为NULL

 ○ 在对一个指针进行间接引用的时候,请先判断该指针是否为NULL。


●  杜绝野指针:


指向一堆垃圾内存的指针。if等判断对它们不起作用,因为没有设置为NULL


一般有三种情况:


 ○ 指针变量没有初始化

 ○ 已经释放不用的指针没有设置为NULL,如delete和free之后的指针

 ○ 指针操作超越了变量作用范围


使用注意事项:


 ○ 没有初始化的,不用的或者超出范围的指针请把值置为NULL。

#include <iostream>
using namespace std;
int main()
{
  // 指针的指针
  int a = 123;
  int* b = &a;
  int** c = &b;
  // NULL 的使用
  int* pA = NULL;
  pA = &a;
  if (pA != NULL)  //  判断NULL指针
  {
    cout << (*pA) << endl;
  }
  pA = NULL;       //  pA不用时,置为NULL
    return 0;
}

原始指针的基本运算

●  &与*操作符

#include <iostream>
using namespace std;
int main()
{
  char ch = 'a';
  // &操作符
  //&ch = 97;                      // &ch左值不合法
  char* cp = &ch;                // &ch右值
  //&cp = 97;                      // &cp左值不合法
  char** cpp = &cp;            // &cp右值
  // *操作符
  *cp = 'a';                           // *cp左值取变量ch位置
  char ch2 = *cp;                 // *cp右值取变量ch存储的值
  //*cp + 1 = 'a';                 //  *cp+1左值不合法的位置
  ch2 = *cp + 1;                  //  *cp+1右值取到的字符做ASCII码+1操作
  *(cp + 1) = 'a';                  //  *(cp+1)左值语法上合法,取ch后面位置
  ch2 = *(cp + 1);                //  *(cp+1)右值语法上合法,取ch后面位置的值
    return 0;
}

●  ++与–操作符

int main()
{
  char ch = 'a';
  char* cp = &ch;
  // ++,--操作符
  char* cp2 = ++cp;
  char* cp3 = cp++;
  char* cp4 = --cp;
  char* cp5 = cp--;
  // ++ 左值
  //++cp2 = 97;
  //cp2++ = 97;
  // *++, ++*
  *++cp2 = 98;
  char ch3 = *++cp2;
  *cp2++ = 98;
  char ch4 = *cp2++;
  // ++++, ----操作符等
  int a = 1, b = 2, c, d;
  //c = a++b;                  // error
  c = a++ + b;
  //d = a++++b;             // error
  char ch5 = ++*++cp;
    return 0;
}

存储区域划分


栈和队列

 ●  栈:先进后出

 ●  队列:先进先出


代码在内存单元中的分布

#include <string>
int a = 0;                                                //(GVAR)全局初始化区 
int* p1;                                                   //(bss)全局未初始化区 
int main()                                               //(text)代码区
{
  int b=1;                                              //(stack)栈区变量 
  char s[] = "abc";                                 //(stack)栈区变量
  int*p2=NULL;                                     //(stack)栈区变量
  char *p3 = "123456";               //123456\0在常量区, p3在(stack)栈区
  static int c = 0;                                   //(GVAR)全局(静态)初始化区 
  p1 = new int(10);                               //(heap)堆区变量
  p2 = new int(20);                               //(heap)堆区变量
  char* p4 = new char[7];                     //(heap)堆区变量
  strcpy_s(p4, 7, "123456");                  //(text)代码区
  //(text)代码区
  if (p1 != NULL)
  {
    delete p1;
    p1 = NULL;
  }
  if (p2 != NULL)
  {
    delete p2;
    p2 = NULL;
  }
  if (p4 != NULL)
  {
    delete[ ] p4;
    p4 = NULL;
  }
  //(text)代码区
  return 0;                                            //(text)代码区
}

从高地址到低地址


栈空间 - 从高到低


堆空间 - 从低到高


cpp动态分配资源和回收原则

动态分配资源 – 堆(heap)


1.从现代编程语言角度,使用堆或者说使用动态内存分配,是一件很自然的事情

2.动态分配内存带来了不确定性:内存分配耗时?失败怎么办?

3.一般而言,当我们在堆上分配内存时,很多语言会使用new这样的关键字,有些语言则是隐式分配。在C++中new的对应词是delete,因为C++是可以让程序员完全接管内存分配释放的。


内存分配和回收原则:


程序通常需要牵涉到三个内存管理器操作:


 ●  分配一个某大小的内存块

 ●  释放一个之前分配的内存块

 ●  垃圾收集操作,寻找不再使用的内存块并且予以释放;


这个回收策略需要实现实时性、额外开销等各方面的平衡,很难有统一和高效的做法;


C++做了1, 2 两件事;java做了1,3两件事。


资源管理方案-RAII

 ●  C++所特有的的资源管理方式。

 ●  RAII依托栈和析构函数,来对所有资源(包括堆内存在内)进行管理。

 ●  RAII有些比较成熟的智能指针的代表:std::auto_ptr 等


C++中几种变量对比

栈和堆中的变量对比:


栈区 堆区
作用域 函数体内,语句块{}作用域 整个程序范围内,由 new malloc开始 , delete free结束
编译期间大小确定 由变量大小范围确定 变量大小范围不确定,需要运行时确定
大小范围 windows默认是1M linux是8M或者10M(可调整) 所有系统的堆内存空间上限是接近内存(虚拟内存)的总大小的(一部分被OS占用)
内存分配方式 地址由高到低 地址由低到高
内容是否可变 可变 可变

全局静态存储区和常量存储区的变量对比:


全局经验存储区 常量存储区
存储内容 全局变量,静态变量 常量
编译期间大小是否确定 确定 确定
内容是否可变 可变 不可变

内存泄漏

   ●  什么事内存泄漏问题:


指的是程序中已经分配的堆内存由于某种原因程序未释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。


   ●  内存泄漏发生的原因和排查方式:

       ○  内存泄漏主要发生在堆内存分配方式中,即配置了内存后,多有指向该内存的指针都遗失了。若缺乏语言这样的垃圾回收机制,这样的内存片无法归还系统。

       ○  因为内存泄漏属于程序运行中的问题,无法 通过编译识别,所以只能在程序运行过程中来判断。


智能指针

使用指针是非常危险的行为,可能存在空指针,野指针的问题,并且可能造成内存泄漏问题。可指针又非常高效,所以我们希望以更安全的方式来使用指针。


●  一般有两种典型的方案:

  • 使用更安全的指针 - 智能指针
  • 不使用指针,使用更安全的方式 - 引用


C++的智能指针

●  C++中推出了常用的只能指针


unique_ptr、share_ptr、weak_ptr和C++11中已经废弃的auto_ptr(C++17中删除)


●  这里我们从应用场景中来分析智能指针


1.应用场景: 对象所有权 生命周期

2.性能分析


auto_ptr

由new expression获得对象,在auto_ptr对象销毁时,它所管理的对象也会被自动delete掉


所有权转移:不小心把它传递给另外的智能指针,原来的指针就不再拥有这个对象了,在拷贝/赋值的过程中,会直接剥夺指针对内存的控制权,转交给新对象,然后再将原对象置为nullptr

#include <string>
#include <iostream>
#include <memory>
using namespace std;
int main()
{
  {// 确定auto_ptr失效的范围
    // 对int使用
    auto_ptr<int> pI(new int(10));
    cout << *pI << endl;                // 10 
    // auto_ptr C++ 17中移除 拥有严格对象所有权语义的智能指针
    // auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,
    // 然后再将原对象指针置为nullptr(早期:NULL)。这种做法也叫管理权转移。
    // 他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,
    // 很多企业在其库内也是要求不准使用auto_ptr。
    auto_ptr<string> languages[5] = {
      auto_ptr<string>(new string("C")),
      auto_ptr<string>(new string("Java")),
      auto_ptr<string>(new string("C++")),
      auto_ptr<string>(new string("Python")),
      auto_ptr<string>(new string("Rust"))
    };
    cout << "There are some computer languages here first time: \n";
    for (int i = 0; i < 5; ++i)
    {
      cout << *languages[i] << endl;
    }
    auto_ptr<string> pC;
    pC = languages[2]; // languges[2] loses ownership. 将所有权从languges[2]转让给pC,
    //此时languges[2]不再引用该字符串从而变成空指针
    cout << "There are some computer languages here second time: \n";
    for (int i = 0; i < 2; ++i)
    {
        cout << *languages[i] << endl;
    }
    cout << "The winner is " << *pC << endl;
    //cout << "There are some computer languages here third time: \n";
    //for (int i = 0; i < 5; ++i)
    //{
    //  cout << *languages[i] << endl;
    //}
  }
  return 0; 
}

unique_ptr

unique_ptr是专属版本,所以unique_ptr管理的内存,只能被一个对象持有,不支持复制和赋值


移动语义:unique_ptr禁止了拷贝语义,但有时我们也需要能够转移所有权,于是提供了移动语义,即可以使用std::move()进行控制所有权的转移。

#include <memory>
#include <iostream>
using namespace std;
int main()
{
  // 在这个范围之外,unique_ptr被释放
  {
    auto i = unique_ptr<int>(new int(10));
    cout << *i << endl;
  }
  // unique_ptr
  auto w = std::make_unique<int>(10);
  cout << *(w.get()) << endl;                             // 10
  //auto w2 = w; // 编译错误如果想要把 w 复制给 w2, 是不可以的。
  //  因为复制从语义上来说,两个对象将共享同一块内存。
  // unique_ptr 只支持移动语义, 即如下
  auto w2 = std::move(w); // w2 获得内存所有权,w 此时等于 nullptr
  cout << ((w.get() != nullptr) ? (*w.get()) : -1) << endl;       // -1
  cout << ((w2.get() != nullptr) ? (*w2.get()) : -1) << endl;   // 10
    return 0;
}

share_ptr 和 weak_ptr

share_ptr通过一个引用计数共享一个对象。


share_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销。


当引用计数为0的时候,该对象没有被使用,可以进行析构。


   ●  循环引用的问题:引用计数会带来循环引用的问题,造成内存无法正常回收,造成内存泄漏。


weak_ptr被设计为与share_ptr共同工作,用一种观察者模式工作。


作用是协助share_ptr工作,可获得资源的观测权,像旁观者那样观测资源使用情况。观察者意味着weak_ptr只对share_ptr进行引用,而不改变其引用计数,当被观察的share_ptr失效后相应的weak_ptr也会失效。

#include <iostream>
#include <memory>
using namespace std;
int main()
{
   shared_ptr 
  //{
  //  //shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。
  //  auto wA = shared_ptr<int>(new int(20));
  //  {
  //    auto wA2 = wA;
  //    cout << ((wA2.get() != nullptr) ? (*wA2.get()) : -1) << endl;       // 20
  //    cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl;           // 20
  //    cout << wA2.use_count() << endl;                                              // 2
  //    cout << wA.use_count() << endl;                                                // 2
  //  }
  //  //cout << wA2.use_count() << endl;                                               
  //  cout << wA.use_count() << endl;                                                    // 1
  //  cout << ((wA.get() != nullptr) ? (*wA.get()) : -1) << endl;               // 20
  //  //shared_ptr 内部是利用引用计数来实现内存的自动管理,每当复制一个 shared_ptr,
  //  //  引用计数会 + 1。当一个 shared_ptr 离开作用域时,引用计数会 - 1。
  //  //  当引用计数为 0 的时候,则 delete 内存。
  //}
  // move 语法
  auto wAA = std::make_shared<int>(30);
  auto wAA2 = std::move(wAA); // 此时 wAA 等于 nullptr,wAA2.use_count() 等于 1
  cout << ((wAA.get() != nullptr) ? (*wAA.get()) : -1) << endl;          // -1
  cout << ((wAA2.get() != nullptr) ? (*wAA2.get()) : -1) << endl;      // 30
  cout << wAA.use_count() << endl;                                                  // 0
  cout << wAA2.use_count() << endl;                                                // 1
  //将 wAA 对象 move 给 wAA2,意味着 wAA 放弃了对内存的所有权和管理,此时 wAA对象等于 nullptr。
  //而 wAA2 获得了对象所有权,但因为此时 wAA 已不再持有对象,因此 wAA2 的引用计数为 1。
    return 0;
}
#include <string>
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
  shared_ptr<B> pb;
  ~A()
  {
    cout << "~A()" << endl;
  }
};
struct B {
  shared_ptr<A> pa;
  ~B()
  {
    cout << "~B()" << endl;
  }
};
// pa 和 pb 存在着循环引用,根据 shared_ptr 引用计数的原理,pa 和 pb 都无法被正常的释放。
// weak_ptr 是为了解决 shared_ptr 双向引用的问题。
struct BW;
struct AW
{
  shared_ptr<BW> pb;
  ~AW()
  {
    cout << "~AW()" << endl;
  }
};
struct BW
{
  weak_ptr<AW> pa;
  ~BW()
  {
    cout << "~BW()" << endl;
  }
};
void Test()
{
  cout << "Test shared_ptr and shared_ptr:  " << endl;
  shared_ptr<A> tA(new A());                                               // 1
  shared_ptr<B> tB(new B());                                                // 1
  cout << tA.use_count() << endl;
  cout << tB.use_count() << endl;
  tA->pb = tB;
  tB->pa = tA;
  cout << tA.use_count() << endl;                                        // 2
  cout << tB.use_count() << endl;                                        // 2
}
void Test2()
{
  cout << "Test weak_ptr and shared_ptr:  " << endl;
  shared_ptr<AW> tA(new AW());
  shared_ptr<BW> tB(new BW());
  cout << tA.use_count() << endl;                                        // 1
  cout << tB.use_count() << endl;                                        // 1
  tA->pb = tB;
  tB->pa = tA;
  cout << tA.use_count() << endl;                                        // 1
  cout << tB.use_count() << endl;                                        // 2
}
int main()
{
  Test();
  Test2();
    return 0;
}

C++的引用

引用是什么?变量的别名,是一种特殊的指针,不允许修改的指针。


使用指针有哪些坑:


1.空指针

2.野指针

3.不知不觉改变了指针的值,却继续使用


使用引用则可以:


1.不存在空引用

2.必须初始化

3.一个引用永远指向它的初始化的那个对象


   ●  基本使用:可以认为是变量的别名,使用时候可以认为是变量本身


有了指针为什么还要有引用?


为了支持运算符重载。


有了引用为什么还需要指针?


为了兼容C语言。

#include <iostream>
#include <assert.h>
using namespace std;
// 编写一个函数,输入两个int型变量a,b
// 实现在函数内部将a,b的值进行交换。
void swap(int& a, int& b)
{
  int tmp = a;
  a = b;
  b = tmp;
}
void swap2(int* a, int* b)
{
  int tmp = *a;
  *a = *b;
  *b = tmp;
}
int main()
{
  //int x = 1, x2 = 3;
  //int& rx = x;
  //rx = 2;
  //cout << x << endl;
  //cout << rx << endl;
  //rx = x2;
  //cout << x << endl;
  //cout << rx << endl;
  // 交换变量的测试
  int a = 3, b = 4;
  swap(a, b);
  assert(a == 4 && b == 3);
  a = 3, b = 4;
  swap2(&a, &b);
  assert(a == 4 && b == 3);
    return 0;
}

补充:(Effective C++)


1.对内置基础类型,传值更高效;

2.对OO面向对象中自定义的类型而言,在函数中传递const引用更高效。

目录
相关文章
|
20天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
46 4
|
2月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
2月前
|
存储 C++
c++的指针完整教程
本文提供了一个全面的C++指针教程,包括指针的声明与初始化、访问指针指向的值、指针运算、指针与函数的关系、动态内存分配,以及不同类型指针(如一级指针、二级指针、整型指针、字符指针、数组指针、函数指针、成员指针、void指针)的介绍,还提到了不同位数机器上指针大小的差异。
47 1
|
2月前
|
存储 编译器 C语言
C++入门2——类与对象1(类的定义和this指针)
C++入门2——类与对象1(类的定义和this指针)
33 2
|
2月前
|
存储 安全 编译器
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(一)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
2月前
|
存储 C++ 索引
C++函数指针详解
【10月更文挑战第3天】本文介绍了C++中的函数指针概念、定义与应用。函数指针是一种指向函数的特殊指针,其类型取决于函数的返回值与参数类型。定义函数指针需指定返回类型和参数列表,如 `int (*funcPtr)(int, int);`。通过赋值函数名给指针,即可调用该函数,支持两种调用格式:`(*funcPtr)(参数)` 和 `funcPtr(参数)`。函数指针还可作为参数传递给其他函数,增强程序灵活性。此外,也可创建函数指针数组,存储多个函数指针。
|
3月前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
48 3
|
2月前
|
算法 C++
【算法】双指针+二分(C/C++
【算法】双指针+二分(C/C++
|
2月前
|
存储 编译器 程序员
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值
|
3月前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。