当前位置: 首页>>技术教程>>正文


C++:了解指针

作者:鲁伯斯·伦德克

日期:2009年9月10日

更新:2010年4月3日-创建了第7.2节,以阐明C++中的指针与数组之间的关系

介绍

本文面向所有希望了解C++语言指针的所有级别的编程爱好者。这里提供的所有代码都不是特定于编译器的,并且所有示例都将以普通ANSI C++编写。关于指针的争论可能绵延数英里,您需要走很长一段路才能掌握所有知识。如果您真的想跑那么远,本文将使您对指针的基本概念有一个清晰的了解,并为您的旅程做准备。但是,对于C++编程不熟悉的人请确保您能够编写和运行自己的C++ “hello world”程序,并且建议您对C++函数和类有基本的了解。如果您需要刷新有关如何编译和运行C++程序,使用函数和类的知识,请阅读附录在继续阅读本文之前,请先阅读本文末尾的内容。

什么是指针?

指针是存储内存地址的变量。 OK,那很简单!但是,什么是内存地址?每个变量都位于计算机内存中的唯一位置下,并且该唯一位置具有其自己的唯一地址,即内存地址。通常,变量保存诸如5或“hello”之类的值,并且这些值存储在计算机内存中的特定位置下。但是,指针是另一种野兽,因为它持有内存地址作为其值,并能够通过使用其关联的内存地址来“point”(因此是指针)指向内存中的某个值。




检索变量的内存地址

好,足够多的交谈,让我们开始讨论指针业务。要检索变量的内存地址,我们需要使用address-of运算符&。

#include <iostream>
int main()
{
using namespace std;
// Declare an integer variable and initialize it with 99
unsigned short int myInt = 99;
// Print out value of myInt
cout << myInt << endl;
// Use address-of operator & to print out
// a memory address of myInt

cout << &myInt << endl;

return 0;
}

OUTPUT:

99
0xbff26312

输出的第一行包含一个整数值99,第二行包含一个打印出的myInt内存地址。请注意,您的输出将有所不同。

将变量的内存地址分配给指针

在为指针分配内存地址之前,我们需要声明一个地址。在C++中声明指针就像声明任何其他具有一个区别的变量一样简单。需要添加Asterix符号“ *”,并将其放置在变量类型之后和变量名称之前。将内存地址分配给指针时,必须遵循一个规则:指针类型必须与其将指向的变量类型匹配。一个例外是指向void的指针,该指针可以处理它将指向的不同类型的变量。为了声明类型为unsigned short int的指针pMark,将使用以下语法:

#include <iostream>

int main()
{
using namespace std;

// Declare and initialize a pointer.
unsigned short int * pPointer = 0;
// Declare an integer variable and initialize it with 35698
unsigned short int twoInt = 35698;
// Declare an integer variable and initialize it with 77
unsigned short int oneInt = 77;
// Use address-of operator & to assign a memory address of twoInt to a pointer
pPointer = &twoInt;
// Pointer pPointer now holds a memory address of twoInt

// Print out associated memory addresses and its values
cout << "pPointer's memory address:\t\t" << &pPointer << endl;
cout << "Integer's oneInt memory address:\t" << &oneInt << "\tInteger value:\t" << oneInt << endl;
cout << "Integer's twoInt memory address:\t" << &twoInt << "\tInteger value:\t" << twoInt << endl;
cout << "pPointer is pointing to memory address:\t" << pPointer << "\tInteger value:\t" << *pPointer << endl;

return 0;
}

OUTPUT:

pPointer's memory address:              0xbff43314
Integer's oneInt memory address: 0xbff43318 Integer value: 77
Integer's twoInt memory address: 0xbff4331a Integer value: 35698
pPointer is pointing to memory address: 0xbff4331a Integer value: 35698

C++ pointer example diagram



上图是对如何在计算机内存中存储变量的高级可视化抽象。指针pPointer从内存地址0xbff43314开始,占用4个字节。指针pPointer将short int twoInt(2个字节)的内存地址作为值保存,该地址为0xbff4331a。该地址作为二进制数据存储在指针的内存空间分配中。因此,取消引用具有内存地址0xbff4331a的指针将间接访问值twoInt,在这种情况下,其值为正整数36698。

访问指针持有的内存地址中的值

正如您在前面的示例中看到的那样,指针pMark真正拥有一个oneInt的值存储地址。通过指针访问变量值的过程称为间接访问,因为变量的值是间接访问的。现在可以使用pPointer指针间接访问oneInt的值。为此,我们需要使用取消引用运算符“ *”来取消对指针的引用,该运算符必须放置在指针变量名称之前:

#include <iostream>

int main()

{
using namespace std;
// Declare an integer variable and initialize it with 99
unsigned short int myInt = 99;
// Declare and initialize a pointer
unsigned short int * pMark = 0;
// Print out a value of myInt
cout << myInt << endl;
// Use address-of operator & to assign a memory address of myInt to a pointer
pMark = &myInt;
// Dereference a pMark pointer with dereference operator * to access a value of myInt
cout << *pMark << endl;

return 0;
}

OUTPUT:

99
99

用指针处理数据

与通过间接访问指针所保存的内存地址处的值相同,也可以使用间接来操作变量的值。将值分配给取消引用的指针将间接更改指针指向的变量的值。以下示例说明了使用指针对数据进行的简单操作:

#include <iostream>

int main()

{
using namespace std;
// Declare an integer variable and initialize it with 99
unsigned short int myInt = 99;
// Declare and initialize a pointer
unsigned short int * pMark = 0;
// Print out a value of myInt
cout << myInt << endl;
// Use address-of operator & to assign memory address of myInt to a pointer
pMark = &myInt;
// Dereference a pMark pointer with dereference operator * and set new value
*pMark = 11;
// show indirectly a value of pMark and directly the value of myInt
cout << "*pMark:\t" << *pMark << "\nmyInt:\t" << myInt << endl;

return 0;
}

OUTPUT:

99
*pMark: 11
myInt: 11

取消引用pMark指针并将新值分配为11会将myInt的值也间接更改为11。再次提醒您,指针仅保留变量的内存地址而不是变量的值很重要。要访问指针所指向的变量的值,必须由取消引用运算符“ *”取消引用。注意“ *”不是乘法,通过C++代码的上下文,如果您打算使用乘法或解引用运算符,则编译器将进行区分。



C++语言中的指针和数组

在C++中,数组名称是指向其第一个元素的常量指针。 c ++指针与数组之间的关系密切相关,其用法几乎可以互换。考虑以下示例:

#include <iostream>

int main()
{
using namespace std;
// Declare an array with 10 elements
int Marks [10]= {1,2,3,4,5,6,7,8,9,0};
// Print out the memory address of an array name
cout << Marks << endl;
// Print out the memory address of a first element of an array
cout << &Marks[0] << endl;
// Print out value of the first element by dereferencing a array name
cout << *Marks << endl;
return 0;
}

OUTPUT:

0xbf83d3fc
0xbf83d3fc
1

如您所见,数组名称确实是指向其第一个元素的指针,因此通过const指针访问数组元素是完全合法的。因此,取消引用数组名称将访问给定数组的第一个元素的值。下一个示例将演示如何通过conts指针访问数组的其他元素。

#include <iostream>

int main()
{
using namespace std;

// Declare an array with 10 elements
int Marks [10]= {1,2,3,4,5,6,7,8,9,0};
// Create a constant pointer to Marks array
const int *pMarks = Marks;
// Access a 6th element of an array by pMarks pointer
cout << *(pMarks + 5) << endl;
// Access a 6th element by dereferencing array name
cout << *(Marks + 5) << endl;
// Access a 6th element of an array
cout << Marks[5] << endl;

return 0;
}

OUTPUT:

6
6
6

数组的第6个元素可以用指针表达式*(Marks + 5)引用。另外,可以声明另一个常量指针,并为其分配数组第一个元素的内存地址的值。然后,该指针的行为与最初声明的Marks数组相同。 “ +”符号告诉编译器从数组的开头移动5个整数对象。如果对象是整数(如本例所示)(整数通常为4个字节),这将导致指针指向第一个数组元素保留的地址后20个字节的内存地址,从而指向第6个。下面的示例详细说明了此想法:

#include <iostream>
int main()
{
using namespace std;
// Declare an array with 10 elements
int Marks [10]= {1,2,3,4,5,6,7,8,9,0};
// Create a constant pointer to Marks array
const int *pMarks = Marks;

for (int i=0, bytes=0; i < 10; ++i, bytes+=4)
{
cout << "Element " << i << ": " << pMarks << " + ";
cout << bytes << " bytes" << " = " << (pMarks + i) << endl;
}
return 0;
}

OUTPUT:

Element 0: 0xbfa5ce0c + 0 bytes = 0xbfa5ce0c
Element 1: 0xbfa5ce0c + 4 bytes = 0xbfa5ce10
Element 2: 0xbfa5ce0c + 8 bytes = 0xbfa5ce14
Element 3: 0xbfa5ce0c + 12 bytes = 0xbfa5ce18
Element 4: 0xbfa5ce0c + 16 bytes = 0xbfa5ce1c
Element 5: 0xbfa5ce0c + 20 bytes = 0xbfa5ce20
Element 6: 0xbfa5ce0c + 24 bytes = 0xbfa5ce24
Element 7: 0xbfa5ce0c + 28 bytes = 0xbfa5ce28
Element 8: 0xbfa5ce0c + 32 bytes = 0xbfa5ce2c
Element 9: 0xbfa5ce0c + 36 bytes = 0xbfa5ce30

考虑以下指针算术运算:
由于我们知道这个特定的数组包含整数,并且整数的长度为4个字节,因此为整个数组保留的内存为40个字节。内存地址0xbfa5ce0c是数组的第一个元素所在的位置。向第一个元素的地址添加20个字节(0x16)将返回第6个元素的地址:0xbfa5ce0c + 0x16(20个字节)= 0xbfa5ce20




指针数组

在C++中,也可以声明一个指针数组。这样的数据结构可用于创建字符串数组(string array)。在C++中,字符串实际上是指向其第一个字符的指针,因此,字符串数组本质上将是指向每个数组元素中字符串的第一个字符的指针的数组。

#include <iostream>
int main()
{
using namespace std;

const char *linuxDistro[6] =
{ "Debian", "Ubuntu", "OpenSuse", "Fedora", "Linux Mint", "Mandriva"};

for ( int i=0; i < 6; i++)
cout << linuxDistro[i] << endl;

return 0;
}

OUTPUT:

Debian
Ubuntu
OpenSuse
Fedora
Linux Mint
Mandriva

指针与数组

添加本节的目的是阐明C++中的指针与数组之间的关系,普通读者可能会跳过它。当努力解释C++中的指针和数组实际上是如何工作时,先前的指针和数组非常相似并且几乎可以互换的说法是一个很好的定义。结果,对这些相似性的理解扩大了程序员对C++的了解。

关于C++中的指针和数组如何以及在何种程度上相关的讨论很多。为了回答这个问题,我们需要询问C++语言的创建者Bjarne Stroustrup。比尼亚·斯特鲁斯特鲁普(Bjarne Stroustrup)在他的书中写道C++编程语言-第三版
在C++中,指针和数组密切相关。数组的名称可用作指向其初始元素的指针。确保将指针指向数组末尾之外的元素可以正常工作。这对于许多算法而言都很重要。但是,由于这样的指针实际上并不指向数组的元素,因此它可能无法用于读取和写入。在初始元素之前获取元素地址的结果是不确定的,应该避免。在某些机器体系结构上,数组通常分配在机器寻址边界上,因此“在初始元素之前一个”根本没有意义。



为什么我们需要指针?

到目前为止,您已经了解了C++指针背后的一些理论和语法。您学习了如何将变量的内存地址分配给指针。为指针分配地址有助于解释指针的工作方式。但是,在现实生活中,您绝不会这样做。此时正确的问题是:如果仅使用变量的声明名称就可以访问和操纵变量,为什么我们需要C++语言的指针? C++通过指针直接访问内存的能力使C++语言优于某些其他语言,例如Visual Basic,C#或Java。直接通过指针而不是通过它们的内存位置访问变量可以提高编写代码的效率和灵活性。但是,可以预见的是,提高效率需要付出成本,因为使用任何low-level工具(例如指针)都意味着实施难度加大。指针最常见的用途包括:

  • 免费商店中的数据管理
  • 访问类成员数据和函数
  • 通过引用传递变量

我们在哪里使用指针?

在本节中,我们将探讨在C++语言中使用指针的几种不同方式。

从”Free Store”分配内存

一旦将返回值传递回调用语句,就将销毁在函数定义内本地声明和使用的变量。这种将值传递给函数的方法的优点是,程序员不必释放由函数的局部变量分配的内存,并且传递给函数的变量不能更改。当功能范围内的声明变量需要由其他功能使用而又不产生通过功能返回值复制这些变量的开销时,就会出现不利之处。

解决此问题的一种方法是创建全局变量。但是,这总是导致代码的可读性,效率和错误降低,因此,应尽可能避免声明全局变量。全局名称空间中已声明的全局变量在整个程序运行时中都持有分配的内存,并且通常只需要很短的时间。

想象一下,程序员将能够在程序运行时动态分配内存。此内存分配也可以在代码中的任何位置使用,并且不再需要在任何时间释放。这就是所谓的“Free Store”出现的地方。空闲存储是供程序员在程序执行期间动态分配的内存和de-allocated。

用以下方式分配内存操作者

需要在免费存储区中分配内存时,将使用new运算符。新操作符的返回值是一个指针。因此,应仅将其分配给指针。这是一个例子:

#include <iostream>
int main()
{
using namespace std;
// Delcare a pointer
unsigned short * pPointer;
// allocate a memory with a new operator on the free store
// and assign a memory address to a pointer named pPointer
pPointer = new unsigned short;
// assign an integer value to a pointer
*pPointer = 31;
// Print out the value of pPointer
cout << *pPointer << endl;

return 0;
}

OUTPUT:

31

仅在实际程序结束时才释放在程序运行时分配的空闲存储内存。对于这个简短的程序,没关系,但是de-allocating并不是免费的内存,这不是一个好的编程习惯,更重要的是,您的代码通常包含11行以上。

De-allocating具有的内存删除操作者

要将分配的内存返回给免费存储,将使用删除操作符:

#include <iostream>
int main()
{
using namespace std;
// Delcare a pointer
unsigned short * pPointer;
// allocate a memory with a new operator on the free store
// and assign a memory address to a pointer named pPointer
pPointer = new unsigned short;
// assign an integer value to a pointer
*pPointer = 31;
// de-allocating memory back to the free store
delete pPointer;
// Print out a value of pPointer
cout << *pPointer << endl;

return 0;
}

OUTPUT:

0

delete运算符释放由新运算符分配的内存,并将其返回到空闲存储。指针的声明仍然有效,因此可以重新分配pPointer。删除运算符只是使用指针删除了关联的内存地址。

注意:重要的是要提到,即使在删除操作符之后,指针也应始终初始化。不这样做可以创建所谓的流浪指针可能会导致不可预测的结果,因为流浪指针可能仍指向内存中的旧位置,并且编译器可以使用该内存地址来存储其他数据。程序可以正常运行。但是,已设置了使程序崩溃的定时炸弹,并且该计时器正在运行。




数据类型类指针

声明类型类数据指针与声明任何其他数据类型的指针没有什么不同。考虑以下示例,在该示例中将构建简单的Heater类来演示此概念。

#include <iostream>

using namespace std;
class Heater
{
// Declare public functions for class heater
public:
// constrator to initialise temperature
Heater( int itsTemperature);
// destructor, takes no action
~Heater();
// accessor function to retrieve temperature
int getTemperature() const;
// declare private data members
private:
int temperature;
};
// definition of constractor
Heater::Heater(int itsTemperature)
{
temperature = itsTemperature;
}
// definition of destructor
Heater::~Heater()
{
// no action taken here
}
// Definition of an accesor function which returns a value of a private Heater class member temperature
int Heater::getTemperature() const
{
return temperature;
}

int main()
{
// define a Heater class pointer object and initialize temperature to 8 by use of constructor
Heater * modelXYZ = new Heater(8);
// Access an accessor function getTemperature() to retrieve temperature via an object modelXVZ
cout << "Temperature of heater modelXYZ is :" << modelXYZ->getTemperature() << endl;
// Free an allocated memory back to a free store
delete modelXYZ;

return 0;
}

OUTPUT:

Temperature of heater modelXYZ is :8

上面代码的最大一部分专用于类定义。从指针的角度来看,对我们来说有趣的部分是主要功能。空闲存储中的内存分配给Heater类的新指针对象modelXYZ。在此代码的下一行,通过使用箭头运算符引用指针对象来调用访问器函数 getTemperature()。最后,释放除了免费存储区之外的内存。重要的是要提到,当一个对象被声明为指针时,不能使用通常用于访问类成员函数的(。)运算符,当一个类对象被声明为指针时,必须使用箭头(->)运算符代替。

使用指针通过引用传递给函数

除了按值和引用传递外,还可以使用指针将参数传递给函数。考虑以下示例:

#include <iostream>

using namespace std;

// FUNCTION PROTOTYPE
// passing to a function by reference using pointers
void addone( int * a, int * b);

int main()
{
// define integers a and b
int a = 1;
int b = 4;

cout << "a = " << a << "\tb = " << b << endl;
// addone() function call
cout << "addone() function call\n";
addone(&a,&b);
// after addone() function call value a=2 and b=5
cout << "a = " << a << "\tb = " << b << endl;

return 0;
}

// addone() fuction header
void addone( int * a, int * b)
{
// addone() function definition
// this function adds 1 to each value
++*a;
++*b;
}

OUTPUT:

a = 1   b = 4
addone() function call
a = 2 b = 5

addone函数的函数原型包含作为可接受的变量类型的指针. addone()函数将1从主函数添加到每个在函数调用期间已传递内存地址的变量。必须知道,使用指向指针的引用作为传递参数,还可以在 addone()函数范围之外更改所涉及的值。这与程序员按值传递给函数的情况完全相反。请参阅附录在本文末尾,列出了有关如何通过值和引用传递给函数的示例。



与指针相关的内存泄漏

正如上面已经提到的,使用指针和新运算符赋予了程序员巨大的力量。强大的力量往往伴随着巨大的责任。释放存储空间的错误操作可能导致内存泄漏。接下来,我们将说明一些与C++指针相关的内存泄漏的实例。

重新分配指针导致内存泄漏

当不再需要从免费存储区分配的内存并且删除操作员不释放它时,就会发生内存泄漏。发生这种情况的一种方法是,在删除运算符有机会执行其工作之前重新分配一个指针:

#include <iostream>
int main()
{
using namespace std;
// Delcare a pointer
unsigned short * pPointer;
// allocate a memory with a new operator on the free store
// and assign a memory address to a pointer named pPointer
pPointer = new unsigned short;
// assign an integer value to a pointer
*pPointer = 31;
// print out the value of pPointer and its associated memory address
cout << "*pPointer Value: " << *pPointer << "\tMemory Address: " << pPointer << endl;
// reassign a pointer to a new memory address from a free store
pPointer = new unsigned short;
// assign an integer value to a pointer
*pPointer = 15;
// print out a value of pPointer and its corresponding memory address
cout << "*pPointer Value: " << *pPointer << "\tMemory Address: " << pPointer << endl;
// de-allocating memory back to the free store
delete pPointer;

return 0;
}

OUTPUT:

*pPointer Value: 31     Memory Address: 0x90e9008
*pPointer Value: 15 Memory Address: 0x90e9018

分析内存泄漏示例的输出:

  1. pPointer拥有一个内存地址(0x90e9008),它指向整数值15。
  2. 通过从空闲存储区分配新的内存,为pPointer重新分配了新的内存地址。
  3. 指向值31的原始内存地址丢失,因此无法释放。
  4. 现在,pPointer拥有一个内存地址(0x90e9018),该地址指向值15的整数。
  5. 地址为0x90e9018的已分配内存为de-allocated。
  6. 由于程序终止,释放了地址为0x90e9008的已分配内存。

滥用局部变量导致内存泄漏

当函数返回值或执行其最后一条语句时,在函数定义内声明的所有变量都将被销毁,并且不再可以从堆栈段中访问。这可能会导致问题:将空闲存储区中的哪些内存分配给在函数定义中本地声明的指针,并且在超出范围之前不会使用delete运算符释放该内存。下一个示例将概述它的发生方式:

#include <iostream>
// function prototype
void localPointer();

int main()
{
// function call from the main function

localPointer();

return
0;
}

// function header

void localPointer()
{
// function definition
// variable declaration pPointer of type pointer
unsigned short int * pPointer;
// allocate a new memory from the free store and assign its memory address to a pointer pPointer
pPointer = new unsigned short int;
// initialise pPointer
*pPointer = 38;
// print out a value to which pPointer is pointing to
std::cout << "Value of *pPointer : " << *pPointer << std::endl;
// de-allocation statement "delete pPointer;" is missing here

// function end
}

OUTPUT:

Value of *pPointer : 38

当function localPointer()退出时,将删除存在于函数范围内的所有变量。 pPointer还是在本地声明的变量,因此,与免费存储中分配的内存关联的内存地址将丢失,因此不可能de-allocate。该程序将在此简单示例中正常运行。但是,每次调用函数 localPointer()时,都会留出一个新内存而不释放它,这将降低程序的效率并导致程序崩溃。有关内存泄漏的更多示例,请参见



结论

正如前面已经提到的,指针为掌握它们的任何软件工程师赋予了强大的力量。本文尝试通过使用示例以简单的方式介绍指针,以便读者可以很好地实现该目标。通过指针直接访问内存是C++语言的最佳功能之一,因此,对于嵌入式设备编程而言,这也是首选的语言。使用指针时,必须格外小心,因为强大的功能可能会迅速变成灾难。

有用的链接:

这里有一些很棒的链接,可以增进对C++指针的理解。

附录

编写,编译和执行C++ “hello world”

#include <iostream>

int main()
{
using namespace std;
cout << "Hello world\n";

return 0;
}

编译:

g++ hello-world.cc -o hello-world

执行:

./hello-world

OUTPUT:

Hello World


C++函数基础

请注意,以下程序仅用于修订目的。它不应作为C++函数的完整指南。请访问以下链接,以更好地了解如何使用C++函数:

#include <iostream>

using namespace std;

// FUNCTION PROTOTYPES

// passing to a function by value
int add( int a, int b);
// passing to a function by reference using pointers
void addone( int * a, int * b);
// passing to a function by reference using reference
void swap( int &a, int &b);

int main()

{
// define integers a and b
int a = 1;
int b = 4;

// add() function call return value will be printed out
cout << "add() function\n";
cout << add(a,b) << endl;
cout << "a = " << a << "\tb = " << b << endl;
// addone() function call
cout << "addone() function\n";
addone(&a,&b);
// after addone() function call value a=2 and b=5
cout << "a = " << a << "\tb = " << b << endl;
// swap() function call
cout << "swap() function\n";
swap(a,b);
// after swap() function call value a=5 and b=2
cout << "a = " << a << "\tb = " << b << endl;

return 0;
}
// add() fuction header
int add( int a, int b)
{
// add() function definition
// this function returns sum of two integers
return a + b;
}

// addone() fuction header
void addone( int * a, int * b)
{
// addone() function definition
// this function adds 1 to each value
++*a;
++*b;
}

// swap() function header
void swap( int &a, int &b)
{
// swap() function definition
// this function swaps values
int tmp;

tmp = a;
a = b;
b = tmp;
}

OUTPUT:

add() function
5
a = 1 b = 4
addone() function
a = 2 b = 5
swap() function
a = 5 b = 2


C++类基础知识

请注意,以下程序仅用于修订目的。它不应作为C++类的完整指南。请访问以下链接,以更好地了解如何使用C++类:

#include <iostream>

using namespace std;
class Heater
{
// declare public functions for class heater
public:
// constrator to initialize a private data member temperature
Heater( int itsTemperature);
// destructor, takes no action
~Heater();
// accessor function to retrieve a private date member temperature
int getTemperature() const;
// declare private data members
private:
int temperature;
};
// definition of constractor
Heater::Heater(int itsTemperature)
{
temperature = itsTemperature;
}
// definition of destructor
Heater::~Heater()
{
// no action taken
}
// definition of accesor function which returns value of a private Heater class member temperature
int Heater::getTemperature() const
{
return temperature;
}

int main()
{
// define a Heater class object and initialise temperature to 8 by calling a constructor
Heater modelXYZ(8);
// access accessor function getTemperature() to retrieve temperature via object modelXVZ
cout << "Temperature of heater modelXYZ is :" << modelXYZ.getTemperature() << endl;
return 0;

}

OUTPUT:

Temperature of heater modelXYZ is :8

参考资料

本文由Ubuntu问答整理, 博文地址: https://ubuntuqa.com/article/7398.html,未经允许,请勿转载。