第四章 数组和指针 (part2) 引入指针

本文详细介绍了C++中的指针概念,包括指针的基本定义、初始化、算术操作及与数组的关系等内容。文中解释了如何声明和使用指针,并讨论了指针与迭代器的相似性以及指针与const限定符的交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

vector 的遍历可使用下标或迭代器实现,同理,也可用下标或指针来遍历数组。

指针是指向某种类型对象的复合数据类型,是用于数组的迭代器:指向数组中的一个元素。

在指向数组元素的指针上使用解引用操作符 *(dereference operator)和自增操作符 ++(increment operator),与在迭代器上的用法类似。

对指针进行解引用操作,可获得该指针所指对象的值。

而当指针做自增操作时,则移动指针使其指向数组中的下一个元素。


 什么是指针
指针的概念很简单:指针用于指向对象。与迭代器一样, 指针提供对其所指对象的间接访问,只是指针结构更通用一些。

与迭代器不同的是,指针用于指向单个对象,而迭代器只能用于访问容器内的元素。

具体来说,指针保存的是另一个对象的地址:

          string s("hello world");
          string *sp = &s; // sp holds the address of s

第二条语句定义了一个指向 string 类型的指针 sp,并初始化 sp 使其指向 string 类型的对象s。

*sp 中的 * 操作符表明 sp 是一个指针变量,&s 中的 & 符号是取地址操作符,当此操作符用于一个对象上时,返回的是该对象的存储地址。

取地址操作符只能用于左值,因为只有当变量用作左值时,才能取其地址。

同样地,由于用于 vector 类型、string 类型或内置数组的下标操作和解引用操作生成左值,因此可对这两种操作的结果做取地址操作,这样即可获取某一特定对象的存储地址。

建议:尽量避免使用指针和数组 

指针和数组容易产生不可预料的错误。其中一部分是概念上的问题:指针用于低级操作,容易产生与繁琐细节相关的(bookkeeping)错误。

其他错误则源于使用指针的语法规则,特别是声明指针的语法。

许多有用的程序都可不使用数组或指针实现,现代C++程序采用vector类型和迭代器取代一般的数组、采用string类型取代C风格字符串。

指针的定义和初始化
每个指针都有一个与之关联的数据类型,该数据类型决定了指针所指向的对象的类型。例如,一个  int 型指针只能指向  int 型对象。

指针变量的定义

C++ 语言使用 * 符号把一个标识符声明为指针:

          vector<int>   *pvec;      // pvec can point to a vector<int>
          int           *ip1, *ip2; // ip1 and ip2 can point to an int
          string        *pstring;   // pstring can point to a string
          double        *dp;        // dp can point to a double
<Tips>:

理解指针声明语句时,请从右向左阅读。

从右向左阅读 pstring 变量的定义,可以看到

          string *pstring;

语句把 pstring 定义为一个指向 string 类型对象的指针变量。类似地,语句

          int *ip1, *ip2; // ip1 and ip2 can point to an int

把 ip1 和 ip2 都定义为指向 int 型对象的指针。

在声明语句中,符号 * 可用在指定类型的对象列表的任何位置:

          double dp, *dp2; // dp2 is a ponter, dp is an object: both type double

该语句定义了一个 double 类型的 dp 对象以及一个指向 double 类型对象的指针dp2。


另一种声明指针的风格

在定义指针变量时,可用空格将符号 * 与其后的标识符分隔开来。下面的写法是合法的:

          string* ps; // legal but can be misleading

也就是说,该语句把 ps 定义为一个指向 string 类型对象的指针。

这种指针声明风格容易引起这样的误解:把 string* 理解为一种数据类型,认为在同一声明语句中定义的其他变量也是指向 string 类型对象的指针。然而,语句

          string* ps1, ps2; // ps1 is a pointer to string,  ps2 is a string

实际上只把 ps1 定义为指针,而 ps2 并非指针,只是一个普通的 string 对象而已。

如果需要在一个声明语句中定义两个指针,必须在每个变量标识符前再加符号 * 声明:

          string* ps1, *ps2; // both ps1 and ps2 are pointers to string

连续声明多个指针易导致混淆

连续声明同一类型的多个指针有两种通用的声明风格。其中一种风格是一个声明语句只声明一个变量,此时,符号 * 紧挨着类型名放置,强调这个声明语句定义的是一个指针:

          string* ps1;
          string* ps2;

另一种风格则允许在一条声明语句中声明多个指针,声明时把符号 * 靠近标识符放置。这种风格强调对象是一个指针:

          string *ps1, *ps2;
<Tips>:

关于指针的声明,不能说哪种声明风格是唯一正确的方式,重要的是选择一种风格并持续使用。

一般建议第二种声明风格:将符号 * 紧贴着指针变量名放置。

指针可能的取值

一个有效的指针必然是以下三种状态之一:

保存一个特定对象的地址;

指向某个对象后面的另一对象;或者是0值。若指针保存0值,表明它不指向任何对象。

未初始化的指针是无效的,直到给该指针赋值后,才可使用它。

下列定义和赋值都是合法的:

          int ival = 1024;
          int *pi = 0;       // pi initialized to address no object
          int *pi2 = & ival; // pi2 initialized to address of ival
          int *pi3;          // ok, but dangerous, pi3 is uninitialized
          pi = pi2;          // pi and pi2 address the same object, e.g. ival
          pi2 = 0;           // pi2 now addresses no object

《强烈关注:》避免使用未初始化的指针

很多运行时错误都源于使用了未初始化的指针。

就像使用其他没有初始化的变量一样, 使用未初始化的指针时的行为几乎总会导致运行时崩溃。

而且,导致崩溃的这一原因很难发现。

对大多数的编译器来说,如果使用未初始化的指针,会将指针中存放的不确定值视为地址,然后操纵该内存地址中存放的位内容。

使用未初始化的指针相当于操纵这个不确定地址中存储的基础数据。因此,在对未初始化的指针进行解引用时,通常会导致程序崩溃。

C++ 语言无法检测指针是否未被初始化,也无法区分有效地址和由指针分配到的存储空间中存放的二进制位形成的地址

建议程序员在使用之前初始化所有的变量,尤其是指针。

如果可能的话,除非所指向的对象已经存在,否则不要先定义指针,这样可避免定义一个未初始化的指针。

如果必须分开定义指针和其所指向的对象,则将指针初始化为 0。因为编译器可检测出 0 值的指针,程序可判断该指针并未指向一个对象。


指针初始化和赋值操作的约束

对指针进行初始化或赋值只能使用以下四种类型的值

  1. 0 值常量表达式,例如,在编译时可获得 0 值的整型 const 对象或字面值常量 0。

  2. 类型匹配的对象的地址。

  3. 另一对象末的下一地址。

  4. 同类型的另一个有效指针。

把 int 型变量赋给指针是非法的,尽管此 int 型变量的值可能为 0。但允许把数值 0 或在编译时可获得 0 值的 const 量赋给指针:

          int ival;
          int zero = 0;
          const int c_ival = 0;
          int *pi = ival; // error: pi initialized from int value of ival
          pi = zero;      // error: pi assigned int value of zero
          pi = c_ival;    // ok: c_ival is a const with compile-time value of 0
          pi = 0;         // ok: directly initialize to literal constant 0

除了使用数值0或在编译时值为 0 的 const 量外,还可以使用 C++ 语言从 C 语言中继承下来的预处理器变量 NULL,该变量在 cstdlib 头文件中定义,其值为 0。

如果在代码中使用了这个预处理器变量,则编译时会自动被数值 0 替换。因此,把指针初始化为 NULL 等效于初始化为 0 值:

          // cstdlib #defines NULL to 0
          int *pi = NULL; // ok: equivalent to int *pi = 0;<更加常用>

正如其他的预处理器变量一样,不可以使用 NULL 这个标识符给自定义的变量命名。

<Tips>:

预处理器变量不是在 std 命名空间中定义的,因此其名字应为 NULL,而非 std::NULL

double dval;
          double *pd = &dval;   // ok: initializer is address of a double
          double *pd2 = pd;     // ok: initializer is a pointer to double

          int *pi = pd;   // error: types of pi and pd differ
          pi = &dval;     // error: attempt to assign address of a double to int *
由于指针的类型用于确定指针所指对象的类型,因此初始化或赋值时必须保证类型匹配。指针用于间接访问对象,并基于指针的类型提供可执行的操作,例如, int 型指针只能把其指向的对象当作  int 型数据来处理,如果该指针确实指向了其他类型(如  double 类型)的对象,则在指针上执行的任何操作都有可能出错。


void* 指针

C++ 提供了一种特殊的指针类型 void*,它可以保存任何类型对象的地址:

double obj = 3.14;
          double *pd = &obj;
          // ok: void* can hold the address value of any data pointer type
          void *pv = &obj;       // obj can be an object of any type
          pv = pd;               // pd can be a pointer to any type

void* 表明该指针与一地址值相关,但不清楚存储在此地址上的对象的类型。

void* 指针只支持几种有限的操作:

与另一个指针进行比较;

向函数传递 void* 指针或从函数返回 void* 指针;

给另一个 void* 指针赋值。

不允许使用 void* 指针操纵它所指向的对象。


指针操作

指针提供间接操纵其所指对象的功能。与对迭代器进行解引用操作一样,对指针进行解引用可访问它所指的对象,* 操作符(解引用操作符)将获取指针所指的对象:

          string s("hello world");
          string *sp = &s; // sp holds the address of s
          cout  <<*sp;     // prints hello world

sp 进行解引用将获得 s 的值,然后用输出操作符输出该值,于是最后一条语句输出了 s 的内容 hello world

生成左值的解引用操作

解引用操作符返回指定对象的左值,利用这个功能可修改指针所指对象的值:

          *sp = "goodbye"; // contents of s now changed

因为 sp 指向 s,所以给 *sp 赋值也就修改了 s 的值。

也可以修改指针 sp 本身的值,使 sp 指向另外一个新对象:

          string s2 = "some value";
          sp = &s2;  // sp now points to s2

给指针直接赋值即可修改指针的值——不需要对指针进行解引用。


关键概念:给指针赋值或通过指针进行赋值

对于初学指针者,给指针赋值和通过指针进行赋值这两种操作的差别确实让人费解。

谨记区分的重要方法是:

如果对左操作数进行解引用,则修改的是指针所指对象的值;

如果没有使用解引用操作,则修改的是指针本身的值。如图所示,帮助理解下列例子:



指针和引用的比较
虽然使用引用(reference)和指针都可间接访问另一个值, 但它们之间有两个重要区别。

第一个区别在于:引用总是指向某个对象:定义引用时没有初始化是错误的。

第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。

引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)

考虑以下两个程序段。第一个程序段将一个指针赋给另一指针:

          int ival = 1024, ival2 = 2048;
          int *pi = &ival, *pi2 = &ival2;
          pi = pi2;    // pi now points to ival2

赋值结束后,pi 所指向的 ival 对象值保持不变,赋值操作修改了 pi 指针的值,使其指向另一个不同的对象。现在考虑另一段相似的程序,使用两个引用赋值:

          int &ri = ival, &ri2 = ival2;
          ri = ri2;    // assigns ival2 to ival

这个赋值操作修改了 ri 引用的值 ival 对象,而并非引用本身。赋值后,这两个引用还是分别指向原来关联的对象,此时这两个对象的值相等。


指向指针的指针

指针本身也是可用指针指向的内存对象。指针占用内存空间存放其值,因此指针的存储地址可存放在指针中。下面程序段:

          int ival = 1024;
          int *pi = &ival; // pi points to an int
          int **ppi = &pi; // ppi points to a pointer to int

定义了指向指针的指针。C++ 使用 ** 操作符指派一个指针指向另一指针。这些对象可表示为:


ppi 进行解引用照常获得 ppi 所指的对象,在本例中,所获得的对象是指向 int 型变量的指针 pi

          int *pi2 = *ppi; // ppi points to a pointer

为了真正地访问到 ival 对象,必须对 ppi 进行两次解引用:

          cout << "The value of ival\n"
               << "direct value: " << ival << "\n"
               << "indirect value: " << *pi << "\n"
               << "doubly indirect value: " << **ppi
               << endl;
这段程序用三种不同的方式输出 ival 的值。

首先,采用直接引用变量的方式输出;

然后使用指向 int 型对象的指针 pi 输出;

最后,通过对 ppi 进行两次解引用获得 ival 的特定值。


使用指针访问数组元素

C++ 语言中,指针和数组密切相关。特别是在表达式中使用数组名时,该名字会自动转换为指向数组第一个元素的指针

          int ia[] = {0,2,4,6,8};
          int *ip = ia; // ip points to ia[0]

如果希望使指针指向数组中的另一个元素,则可使用下标操作符给某个元素定位,然后用取地址操作符 & 获取该元素的存储地址

          ip = &ia[4];    // ip points to last element in ia
指针的算术操作

与其使用下标操作,倒不如通过指针的算术操作来获取指定内容的存储地址。

指针的算术操作和迭代器的算术操作以相同的方式实现(也具有相同的约束)。

使用指针的算术操作在指向数组某个元素的指针上加上(或减去)一个整型数值,就可以计算出指向数组另一元素的指针值:

          ip = ia;            // ok: ip points to ia[0]
          int *ip2 = ip + 4;  // ok: ip2 points to ia[4], the last element in ia

在指针 ip 上加 4 得到一个新的指针,指向数组中 ip 当前指向的元素后的第4 个元素。

通常,在指针上加上(或减去)一个整型数值 n 等效于获得一个新指针,该新指针指向指针原来指向的元素之后(或之前)的第 n 个元素。

<note>:

指针的算术操作只有在原指针和计算出来的新指针都指向同一个数组的元素,或指向该数组存储空间的下一单元时才是合法的。

如果指针指向一对象,我们还可以在指针上加1从而获取指向相邻的下一个对象的指针。


假设数组 ia 只有 4 个元素,则在 ia 上加 10 是错误的:

          // error: ia has only 4 elements, ia + 10 is an invalid address
          int *ip3 = ia + 10;

只要两个指针指向同一数组或有一个指向该数组末端的下一单元,C++ 还支持对这两个指针做减法操作:

          ptrdiff_t n = ip2 - ip; // ok: distance between the pointers

结果是 4,这两个指针所指向的元素间隔为 4 个对象。

两个指针减法操作的结果是标准库类型(library type)ptrdiff_t 的数据。

size_t 类型一样,ptrdiff_t 也是一种与机器相关的类型,在cstddef 头文件中定义。

size_tunsigned 类型,而ptrdiff_t 则是signed 整型。


这两种类型的差别体现了它们各自的用途: size_t 类型用于指明数组长度,它必须是一个正数;ptrdiff_t 类型则应保证足以存放同一数组中两个指针之间的差距,它有可能是负数。例如, ip 减去 ip2,结果为 -4
允许在指针上加减 0,使指针保持不变。

更有趣的是,如果一指针具有 0 值(空指针),则在该指针上加 0 仍然是合法的,结果得到另一个值为 0 的指针。也可以对两个空指针做减法操作,得到的结果仍是 0。

解引用和指针算术操作之间的相互作用

在指针上加一个整型数值,其结果仍然是指针。允许在这个结果上直接进行解引用操作,而不必先把它赋给一个新指针:

          int last = *(ia + 4); // ok: initializes last to 8, the value of ia[4]

这个表达式计算出 ia 所指向元素后面的第 4 个元素的地址,然后对该地址进行解引用操作,等价于 ia[4]

<note>:

加法操作两边用圆括号括起来是必要的。如果写为:

          last = *ia + 4;     // ok: last = 4, equivalent to ia[0]+4

意味着对 ia 进行解引用,获得 ia 所指元素的值 ia[0],然后加 4。

由于加法操作和解引用操作的优先级不同,上述表达式中的圆括号是必要的。简单地说,优先级决定了有多个操作符的表达式如何对操作数分组。解引用操作符的优先级比加法操作符高。

与低优先级的操作符相比,优先级高的操作符的操作数先被组合起来操作。如果没有圆括号,解引用操作符的操作数是 ia,该表达式先对 ia 解引用,获得 ia 数组中的第一个元素,并将该值与 4 相加。

如果表达式加上圆括号,则不管一般的优先级规则,将 (ia + 4) 作为单个操作数,这是 ia 所指向的元素后面第4个元素的地址,然后对这个新地址进行解引用。

下标和指针

我们已经看到,在表达式中使用数组名时,实际上使用的是指向数组第一个元素的指针。这种用法涉及很多方面,当它们出现时我们会逐一指出来。

其中一个重要的应用是使用下标访问数组时,实际上是使用下标访问指针:

          int ia[] = {0,2,4,6,8};
          int i = ia[0]; // ia points to the first element in ia

ia[0] 是一个使用数组名的表达式。在使用下标访问数组时,实际上是对指向数组元素的指针做下标操作。只要指针指向数组元素,就可以对它进行下标操作:

          int *p = &ia[2];     // ok: p points to the element indexed by 2
          int j = p[1];        // ok: p[1] equivalent to *(p + 1),
                               //    p[1] is the same element as ia[3]
          int k = p[-2];       // ok: p[-2] is the same element as ia[0]

vector 类型提供的 end 操作将返回指向超出 vector 末端位置的一个迭代器。这个迭代器常用作哨兵,来控制处理vector 中元素的循环。类似地,可以计算数组的超出末端指针的值:

          const size_t arr_size = 5;
          int arr[arr_size] = {1,2,3,4,5};
          int *p = arr;           // ok: p points to arr[0]
          int *p2 = p + arr_size; // ok: p2 points one past the end of arr
                                  //    use caution -- do not dereference!

本例中,p 指向数组 arr 的第一个元素,在指针 p 上加数组长度即可计算出数组arr 的超出末端指针。p 加 5 即得 p 所指向的元素后面的第五个 int 元素的地址——换句话说,p + 5指向数组的超出末端的位置。

<Note>:

C++ 允许计算数组或对象的超出末端的地址,但不允许对此地址进行解引用操作。而计算数组超出末端位置之后或数组首地址之前的地址都是不合法的。

计算并存储在p2中的地址,与在 vector 上做 end 操作所返回的迭代器具有相同的功能。由end 返回的迭代器标志了该 vector 对象的“超出末端位置”,不能进行解引用运算,但是可将它与别的迭代器比较,从而判断是否已经处理完vector 中所有的元素。

同理,p2 也只能用来与其他指针比较,或者用做指针算术操作表达式的操作数。对 p2 进行解引用将得到无效值。

对大多数的编译器来说,会把对 p2 进行解引用的结果(恰好存储在 arr 数组的最后一个元素后面的内存中的二进制位)视为一个int 型数据。


输出数组元素

用指针编写以下程序:

          const size_t arr_sz = 5;
          int int_arr[arr_sz] = { 0, 1, 2, 3, 4 };
          // pbegin points to first element, pend points just after the last
          for (int *pbegin = int_arr, *pend = int_arr + arr_sz;
                    pbegin != pend; ++pbegin)
              cout << *pbegin << ' '; // print the current element

这段程序使用了一个我们以前没有用过的 for 循环性质:只要定义的多个变量具有相同的类型,就可以在 for 循环的初始化语句中同时定义它们。本例在初始化语句中定义了两个 int 型指针pbeginpend

C++ 允许使用指针遍历数组。

和其他内置类型一样,数组也没有成员函数。因此,数组不提供 beginend 操作,程序员只能自己给指针定位,使之分别标志数组的起始位置和超出末端位置。

可在初始化中实现这两个指针的定位:初始化指针 pbegin 指向 int_arr 数组的第一个元素,而指针pend 则指向该数组的超出末端的位置:


指针 pend 是标志 for 循环结束的哨兵。for 循环的每次迭代都会使pbegin 递增 1 以指向数组的下一个元素。

第一次执行 for 循环时,pbegin 指向数组中的第一个元素;第二次循环,指向第二个元素;这样依次类推。

当处理完数组的最后一个元素后,pbegin 再加 1 则与 pend 值相等,表示整个数组已遍历完毕。

指针是数组的迭代器

聪明的读者可能已经注意到这段程序迭代器的一段程序非常相像,该程序使用下面的循环遍历并输出一个 string 类型的vector 的内容:

          // equivalent loop using iterators to reset all the elements in ivec to 0
          for (vector<int>::iterator iter = ivec.begin();
                                     iter != ivec.end(); ++iter)
              *iter = 0; // set element to which iter refers to 0

这段程序使用迭代器的方式就像上个程序使用指针实现输出数组内容一样。

指针和迭代器的这个相似之处并不是巧合。实际上,内置数组类型具有标准库容器的许多性质,与数组联合使用的指针本身就是迭代器


指针和 const 限定符
指向 const 对象的指针

到目前为止,我们使用指针来修改其所指对象的值。但是如果指针指向 const 对象,则不允许用指针来改变其所指的const

为了保证这个特性,C++ 语言强制要求指向 const 对象的指针也必须具有 const 特性

          const double *cptr;  // cptr may point to a double that is const

这里的 cptr 是一个指向 double 类型 const 对象的指针,const 限定了cptr 指针所指向的对象类型,而并非 cptr 本身

也就是说,cptr 本身并不是 const

在定义时不需要对它进行初始化,如果需要的话,允许给 cptr 重新赋值,使其指向另一个 const 对象。但不能通过cptr 修改其所指对象的值:

          *cptr = 42;   // error: *cptr might be const

把一个 const 对象的地址赋给一个普通的、非 const 对象的指针也会导致编译时的错误:

          const double pi = 3.14;
          double *ptr = &pi;        // error: ptr is a plain pointer
          const double *cptr = &pi; // ok: cptr is a pointer to const

不能使用 void* 指针保存 const 对象的地址,而必须使用const void* 类型的指针保存const 对象的地址:

          const int universe = 42;
          const void *cpv = &universe; // ok: cpv is const
          void *pv = &universe;        // error: universe is const

允许把非 const 对象的地址赋给指向 const 对象的指针,例如:

          double dval = 3.14; // dval is a double; its value can be changed
          cptr = &dval;       // ok: but can't change dval through cptr

尽管 dval 不是 const 对象,但任何企图通过指针 cptr 修改其值的行为都会导致编译时的错误。

cptr 一经定义,就不允许修改其所指对象的值。如果该指针恰好指向非 const 对象时,同样必须遵循这个规则。

<Note>:

不能使用指向 const 对象的指针修改基础对象,然而如果该指针指向的是一个非 const 对象,可用其他方法修改其所指的对象。

事实是,可以修改 const 指针所指向的值,这一点常常容易引起误会。考虑:

          dval = 3.14159;       // dval is not const
          *cptr = 3.14159;      // error: cptr is a pointer to const
          double *ptr = &dval;  // ok: ptr points at non-const double
          *ptr = 2.72;          // ok: ptr is plain pointer
          cout << *cptr;        // ok: prints 2.72
在此例题中,指向 const 的指针 cptr 实际上指向了一个非 const 对象。尽管它所指的对象并非 const,但仍不能使用 cptr 修改该对象的值。

本质上来说,由于没有方法分辩 cptr 所指的对象是否为 const,系统会把它所指的所有对象都视为const

如果指向 const 的指针所指的对象并非 const,则可直接给该对象赋值或间接地利用普通的非const 指针修改其值:毕竟这个值不是 const

重要的是要记住:不能保证指向 const 的指针所指对象的值一定不可修改。

<Tips>:

如果把指向 const 的指针理解为“自以为指向 const 的指针”,这可能会对理解有所帮助。

在实际的程序中,指向 const 的指针常用作函数的形参将形参定义为指向const 的指针,以此确保传递给函数的实际对象在函数中不因为形参而被修改。


const 指针

除指向 const 对象的指针外,C++ 语言还提供了 const 指针——本身的值不能修改:

          int errNumb = 0;
          int *const curErr = &errNumb; // curErr is a constant pointer

我们可以从右向左把上述定义语句读作“curErr 是指向 int 型对象的const 指针”。

与其他 const 量一样,const 指针的值不能修改,这就意味着不能使curErr 指向其他对象

任何企图给 const 指针赋值的行为(即使给 curErr 赋回同样的值)都会导致编译时的错误:

          curErr = curErr; // error: curErr is const

与任何 const 量一样,const 指针也必须在定义时初始化。

指针本身是 const 的事实并没有说明是否能使用该指针修改它所指向对象的值

指针所指对象的值能否修改完全取决于该对象的类型。例如,curErr 指向一个普通的非常量int 型对象 errNumb,则可使用 curErr 修改该对象的值:

          if (*curErr) {
              errorHandler();
              *curErr = 0; // ok: reset value of the object to which curErr is bound
          }

指向 const 对象的 const 指针

还可以如下定义指向 const 对象的 const 指针:

          const double pi = 3.14159;
          // pi_ptr is const and points to a const object
          const double *const pi_ptr = &pi;"

本例中,既不能修改 pi_ptr 所指向对象的值,也不允许修改该指针的指向(即 pi_ptr 中存放的地址值)。

可从右向左阅读上述声明语句:“pi_ptr 首先是一个 const 指针,指向 double 类型的 const 对象”。


指针和 typedef

在 typedef中使用指针往往会带来意外的结果。下面是一个几乎所有人刚开始时都会答错的问题。假设给出以下语句:

          typedef string *pstring;
          const pstring cstr;

请问 cstr 变量是什么类型?简单的回答是 const pstring 类型的指针。进一步问:const pstring 指针所表示的真实类型是什么?很多人都认为真正的类型是:

          const string *cstr; // wrong interpretation of const pstring cstr

也就是说,const pstring 是一种指针,指向 string 类型的 const 对象,但这是错误的。

错误的原因在于将 typedef 当做文本扩展了

声明 const pstring 时,const 修饰的是 pstring 的类型,这是一个指针。

因此,该声明语句应该是cstr 定义为指向 string 类型对象的const 指针,这个定义等价于:

          // cstr is a const pointer to string
          string *const cstr; // equivalent to const pstring cstr

建议:理解复杂的 const 类型的声明

阅读 const 声明语句产生的部分问题,源于const 限定符既可以放在类型前也可以放在类型后

          string const s1;   // s1 and s2 have same type,
          const string s2;   // they're both strings that are const
typedefconst 类型定义时,const 限定符加在类型名前面容易引起对所定义的真正类型的误解:

 string s;
          typedef string *pstring;
          const pstring cstr1 = &s; // written this way the type
 is obscured
          pstring const cstr2 = &s; // all three decreations are
 the same type
          string *const cstr3 = &s; // they're all const pointers
 to string
const 放在类型 pstring 之后,然后从右向左阅读该声明语句就会非常清楚地知道cstr2const pstring 类型,即指向 string 对象的 const 指针

不幸的是,大多数人在阅读 C++ 程序时都习惯看到 const 放在类型前面。于是为了遵照惯例,只好建议编程时把const 放在类型前面。但是,把声明语句重写为置 const 于类型之后更便于理解。




<think>好的,用户想用C++的三个线程来计算1到n的,需要示例代码。首先,我得回忆一下C++多线程的基本用法。记得C++11引入了<thread>库,可以方便地创建线程。用户的问题需要三个线程同时工作,各自处理一部分数据,然后将结果汇总。 首先,得考虑如何分割任务。假设n比较大,可以将1到n分成三段,每个线程处理一段的。比如,线程1处理1到n/3,线程2处理n/3+1到2n/3,线程3处理2n/3+1到n。不过需要注意n可能无法被3整除,所以需要调整每个线程的结束位置,避免漏掉数。 然后,每个线程需要计算自己区间的,并将结果存储起来。因为多个线程会同时修改总,这里需要用互斥锁来保护共享变量,或者使用原子操作。不过,更高效的方法可能是让每个线程计算自己的部分,最后再相加,这样可以避免锁的开销。但用户可能希望每个线程直接累加到一个总变量中,这就需要同步机制。 看用户的引用资料,引用[3]提到使用多线程并行求,但没有用数组,可能用的是全局变量或者传递引用。比如,每个线程计算一部分,然后累加到一个共享变量中,这时候必须用互斥锁来保护这个变量,防止数据竞争。 另外,线程的创建连接。在C++中,创建线程后需要join,等待它们完成。主线程可能需要等待三个子线程完成后,再输出总。 现在,具体步骤: 1. 定义全局变量total,并初始化为0。 2. 定义互斥锁mutex,用于保护total。 3. 每个线程计算指定区间的,累加到total。 4. 主线程创建三个线程,分别处理各自的区间。 5. 主线程等待所有子线程结束,然后输出总。 需要注意的问题包括:区间的划分是否正确,特别是当n不能被3整除时如何处理余数。例如,当n=10时,每个线程处理3、3、4个数。或者使用更均匀的分配方法,比如将余数分配给前几个线程。 另外,线程函数的参数传递可能需要使用结构体或者lambda捕获,但C++线程的参数传递需要是静态的,或者通过引用包装。或者,使用lambda表达式来捕获局部变量,但需要注意作用域生命周期的问题。 比如,可以定义一个函数,接受开始结束的值,以及指向总指针,然后加锁进行累加。或者,每个线程返回自己的部分,主线程汇总,这可能更高效,因为不需要锁。但用户可能希望用共享变量的方式。 根据引用[3],他们的实现是每个线程计算自己的部分,然后主线程汇总。这可能更高效,因为避免了锁竞争。例如,每个线程的结果存放在一个数组中,主线程最后相加。 不过用户的问题要求三个线程,所以可能需要三个不同的函数或同一个函数的不同参数。或者,使用lambda表达式传递不同的区间参数。 综合来看,可能的实现步骤: - 将n分成三个区间,每个线程处理一个区间。 - 每个线程计算自己区间的,存放在独立的变量中。 - 主线程等待所有线程完成后,将三个部分相加。 这种方法不需要锁,因为每个线程操作自己的变量。但需要确保这些变量在主线程中可访问。或者,可以通过参数传递存储结果的引用,但需要注意线程安全。 例如,使用std::promisestd::future来传递结果,或者使用全局数组存储部分。 或者,更简单的方式,每个线程函数接收开始、结束指向部分指针,计算后将结果存入指针位置。主线程创建三个变量,每个线程处理一部分,并将结果存入对应的变量,最后相加。 这可能更简单,不需要锁,因为每个线程操作不同的内存位置。 比如: int部分1,部分2,部分3; 线程1计算区间1,结果存入部分1; 线程2计算区间2,结果存入部分2; 线程3计算区间3,结果存入部分3; 主线程等待,然后总=部分1+部分2+部分3; 这种方式不需要锁,因为每个线程独立写入自己的变量,主线程读取时已经join了,不会有竞争。 这种方法更高效,因为没有锁的开销,适用于多线程并行计算。 所以,示例代码的大致结构: #include <iostream> #include <thread> using namespace std; void sum_part(int start, int end, int &result) { int sum = 0; for(int i=start; i<=end; ++i) { sum +=i; } result = sum; } int main() { int n = 100; int part1, part2, part3; int step = n /3; thread t1(sum_part, 1, step, ref(part1)); thread t2(sum_part, step+1, 2*step, ref(part2)); thread t3(sum_part, 2*step+1, n, ref(part3)); t1.join(); t2.join(); t3.join(); int total = part1 + part2 + part3; cout << total << endl; return 0; } 但这里需要注意,当n无法被3整除时,第三个线程的end应该是n,例如当n=100,每个线程处理1-33,34-66,67-100?不,100/3是33余1,所以可能正确的划分是: step = (n + 2)/3; // 向上取整? 或者,计算每个线程的起始结束: 比如,总共有三个线程,第i个线程处理从 (i-1)*chunk_size +1 到 i*chunk_size,最后一个线程处理到n。 其中,chunk_size = (n +2)/3; 这样确保每个线程大致处理1/3的数。 例如,n=10,chunk_size=4(因为10/3=3.333,向上取整为4)。那么线程1处理1-4,线程2处理5-8,线程3处理9-10。 这样分配更合理。所以代码中需要计算每个区间的startend。 可能需要调整划分方式,确保覆盖所有数。 可能的划分方法: int chunk_size = (n + 2) /3; // 这样每个chunk_size是向上取整的。 线程1:1 到 chunk_size 线程2: chunk_size+1到 2*chunk_size 线程3: 2*chunk_size +1到n 这样可以处理余数的情况。 例如n=10,chunk_size=4: 线程1:1-4 线程2:5-8 线程3:9-10 总是1-10的55。 而如果用原来的step = n/3,即step=3,那么线程3处理到3*3=9,剩下的10未被处理。所以必须调整。 因此,正确的划分应该是: int chunk_size = (n +2)/3; // 或者类似的方式。 或者,让每个线程处理不同的区间: int start1 = 1; int end1 = n/3; int start2 = end1 +1; int end2 = 2*(n/3); int start3 = end2 +1; int end3 =n; 但这样可能不够准确。例如n=10,n/3=3,所以线程1处理1-3,线程2处理4-6,线程3处理7-10。这样总是1-3=6,4-6=15,7-10=34,总6+15+34=55,正确。 所以,正确的划分方法是: 每个线程处理大约1/3的数,但需要确保所有数都被覆盖。 所以,在代码中,正确的划分应该是: int step = n/3; start1=1, end1=step; start2=step+1, end2=2*step; start3=2*step+1, end3=n; 这样,当n=10时,step=3,线程3处理7-10,总正确。当n=9时,每个线程处理3,3,3,正确。 所以,在代码中,应该这样计算每个区间的startend: int step = n /3; int start1 =1; int end1=step; int start2 = step+1; int end2 = 2*step; int start3=2*step +1; int end3=n; 然后创建三个线程,每个处理自己的区间。 但需要考虑n是否足够大,比如n=2,这时候step=0,导致区间错误。但用户的问题可能假设n足够大,或者可以处理这些边界情况。 现在,编写代码: 可能需要在sum_part函数中处理startend,当start>end时返回0。 或者,在分配区间时确保start<=end。 例如,当n=2时,step=0,线程1处理1-0(无效),所以需要调整。 可能更好的方法是计算每个线程的区间,比如: 总共有三个线程,每个处理大致n/3个数,余数分配到前面的线程。 例如,余数r =n%3; 线程1处理 (n/3)+1 如果r>0 线程2处理 (n/3)+1 如果r>1 或者,另一种分配方式: int base = n /3; int remainder =n%3; int end1 = base + (remainder>0 ?1:0); int end2 = end1 + base + (remainder>1 ?1:0); int end3 =n; 这样,每个线程处理不同的区间。例如,n=10: base=3, remainder=1. 线程1:1-4 (3+1) 线程2:5-7 (4+1到4+3=7) 线程3:8-10. 但这样总是1-4=10,5-7=18,8-10=27,总55,正确。 但这种方法可能比较复杂。 或者,更简单的方式是让主线程处理余数。例如,每个线程处理base个数,余数分配给前几个线程。 比如,每个线程处理base个数,其中base =n/3,余数r =n%3. 线程1处理 base +1 如果r>0 线程2处理 base +1 如果r>1 线程3处理 base 例如,n=10,base=3, r=1. 线程1处理4个数(1-4) 线程2处理4个数(5-8) 线程3处理3个数(9-11?超过n=10) 这显然不对。所以可能需要更合理的分配。 另一种方法是使用循环分配余数。比如,对于余数r=1,线程1多处理一个数;r=2,线程1线程2各多处理一个。 所以,正确的分配方式: int base =n/3; int remainder =n%3; int end1 = base + (remainder>0 ?1:0); int end2 = end1 + base + (remainder>1 ?1:0); int end3 =n; 但这样需要计算每个区间的起始结束。 这可能比较复杂,但为了正确性,必须处理。 所以,在代码中,可能需要如下步骤: int n = ...; int base = n /3; int remainder = n%3; int start1 =1; int end1 = base + (remainder>0 ?1 :0); int start2 = end1 +1; int end2 = end1 + base + (remainder>1 ?1 :0); int start3 = end2 +1; int end3 =n; 然后线程1处理start1到end1,线程2处理start2到end2,线程3处理start3到end3. 这样可以确保余数被正确分配。 例如,n=10: base=3, remainder=1. end1=3+1=4. start2=5. end2=4+3=7. start3=8. end3=10. 线程1处理1-4(4个数),线程2处理5-7(3个数),线程3处理8-10(3个数)。总正确。 另一个例子,n=11: base=3, remainder=2. end1=3+1=4. end2=4 +3 +1=8. start3=9. end3=11. 线程1处理1-4(4),线程2处理5-8(4),线程3处理9-11(3)。总=10+26+30=66,而正确总是11*12/2=66。正确。 这样,这样的分配方法是正确的。 所以,代码需要动态计算每个区间的startend,考虑余数的情况。 因此,在代码中,可以这样实现: int n = 100; int base = n /3; int remainder = n%3; int start1 =1; int end1 = base + (remainder>0 ?1 :0); int start2 = end1 +1; int end2 = end1 + base + (remainder>1 ?1 :0); int start3 = end2 +1; int end3 =n; 然后创建三个线程,每个处理自己的区间。 这样,不管n是否能被3整除,都可以正确分配。 现在,将这个逻辑写入代码。 接下来,线程函数的参数传递。在C++中,线程的参数是按值传递的,除非使用std::ref。因此,如果要将部分作为引用传递给线程函数,需要使用std::ref。 例如,sum_part函数接受开始、结束引用参数: void sum_part(int start, int end, int &result) { int sum =0; for(int i=start; i<=end; i++) sum +=i; result = sum; } 然后,在线程创建时: thread t1(sum_part, start1, end1, ref(part1)); thread t2(sum_part, start2, end2, ref(part2)); thread t3(sum_part, start3, end3, ref(part3)); 这样,每个线程的部分会被存入part1, part2, part3。 主线程在join所有线程后,相加得到总。 这样,代码就能正确运行。 现在,考虑可能的边界情况,例如n=0或n=1等。假设用户输入的n是正整数,否则可能需要处理,但示例中可以假设n>=3。 现在,编写完整的代码示例: #include <iostream> #include <thread> using namespace std; void sum_part(int start, int end, int &result) { int sum = 0; for (int i = start; i <= end; ++i) { sum += i; } result = sum; } int main() { int n = 100; // 示例输入 int part1 = 0, part2 = 0, part3 = 0; int base = n / 3; int remainder = n % 3; int start1 = 1; int end1 = base + (remainder > 0 ? 1 : 0); int start2 = end1 + 1; int end2 = end1 + base + (remainder > 1 ? 1 : 0); int start3 = end2 + 1; int end3 = n; thread t1(sum_part, start1, end1, ref(part1)); thread t2(sum_part, start2, end2, ref(part2)); thread t3(sum_part, start3, end3, ref(part3)); t1.join(); t2.join(); t3.join(); int total = part1 + part2 + part3; cout << "Total sum: " << total << endl; return 0; } 这个代码应该能正确计算1到n的,使用三个线程。例如,当n=100时,总应为5050,代码运行结果是否正确? 验证:base=33,remainder=1. end1=33+1=34. 线程1处理1-34,(34*35)/2=595. 线程2处理35到34+33=67,是(67*68/2)-(34*35/2)= (4556 - 595)= 3961? 但实际计算应该是35到67的(35+67)*33/2 = 102*33/2= 51*33=1683. 线程3处理68到100,是(100+68)*33/2= 168*33/2=84*33=2772. 总:595+1683+2772=5050,正确。 所以,代码逻辑正确。 现在,用户可能的问题: 1. 如何确保线程安全?如果使用共享变量是否需要互斥锁? 在这个示例中,每个线程写入自己的变量,没有竞争,因此不需要锁。因为每个线程写入独立的内存位置,主线程在join之后读取,所以是安全的。 2. 如何处理更大的n或动态线程数? 可以扩展为根据线程数动态分配区间,但用户的问题固定为三个线程。 3. 异常处理? 例如,如果输入的n太小,可能导致区间start大于end。例如,n=2,base=0,余数=2. start1=1, end1=0+1=1. start2=2, end2=1 +0 + (余数>1?1:0). 余数是2>1,所以end2=1+0+1=2. start3=3, end3=2. 这会出错,因为start3=3大于end3=2。 所以在sum_part函数中,需要处理这种情况,即当start>end时,sum=0. 所以,修改sum_part函数: void sum_part(int start, int end, int &result) { int sum = 0; if (start <= end) { for (int i = start; i <= end; ++i) { sum += i; } } result = sum; } 这样,当start>end时,sum为0,避免错误。 例如,n=2: base=0, remainder=2. end1=0+1=1. start2=2. end2=1 +0 +1=2. start3=3. end3=2. 线程3处理3到2,sum=0. 总是1-1=1,2-2=2,总3,正确(1+2=3)。 因此,代码现在处理n较小的案例也是正确的。 综上,代码示例应该是正确的,处理了各种情况,包括余数分配小的n值。</think>以下是使用C++通过三个线程计算1到n之的示例代码: ```cpp #include <iostream> #include <thread> using namespace std; // 线程函数:计算从start到end的累加,结果存入result void sum_part(int start, int end, int &result) { int sum = 0; if (start <= end) { for (int i = start; i <= end; ++i) { sum += i; } } result = sum; } int main() { int n = 100; // 待计算的数值范围 int part1 = 0, part2 = 0, part3 = 0; // 划分三个线程的计算区间 int base = n / 3; int remainder = n % 3; int start1 = 1; int end1 = base + (remainder > 0 ? 1 : 0); int start2 = end1 + 1; int end2 = end1 + base + (remainder > 1 ? 1 : 0); int start3 = end2 + 1; int end3 = n; // 创建并启动三个线程 thread t1(sum_part, start1, end1, ref(part1)); thread t2(sum_part, start2, end2, ref(part2)); thread t3(sum_part, start3, end3, ref(part3)); // 等待所有线程完成 t1.join(); t2.join(); t3.join(); // 汇总结果 int total = part1 + part2 + part3; cout << "Total sum: " << total << endl; return 0; } ``` ### 实现说明 1. **任务划分**:将1到n的区间分为三部分,考虑余数的分配: - 第一个线程处理前$\lfloor n/3 \rfloor + 1$(当有余数时) - 第二个线程处理中间$\lfloor n/3 \r
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值