如何将参数传递给函数是C++中的一个重要概念。对于初学者来说,这通常很难理解,因为你没有这些不同的方法来传递Java或JavaScript等语言中的参数。你可以通过值调用或按引用调用将参数传递给函数。它们之间的区别在于,在按值调用时,会创建实际参数的副本,并且被调用的函数在副本上运行。而在通过引用调用时,参数的内存(地址)中的位置被传递给函数。这意味着,该函数将对同一个相同的对象进行操作,因此对象的任何修改都将在函数调用之外保留,因为当函数返回时,更改当然不会被还原。我们来看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #include <iostream> // this is call by value // the integer x is copied and any modification is done on the copy void test_by_value(int x) { x = 1; } // call by reference is done using the `&` after the type // no copy is created, behind the scenes the memory location is passed // and the same number is used void test_by_ref(int& x) { x = 2; } int main() { std::cout << "Hello World!\n"; int number = 0; test_by_value(number); // outputs 0, number unchaged std::cout << "test_by_value " << number << "\n"; test_by_ref(number); // outputs 2(!), number changed std::cout << "test_by_ref " << number << "\n"; } |
请注意,无论你是通过值传递还是通过引用传递,调用者的代码都是完全相同的。调用者总是只是传递number
。对于引用传递,&
符号用作函数参数列表中的指示符。与呼叫方无法区分,因此无法推断你的数据是否被修改为功能的副作用的一部分。C++有这两种类型的原因是C++对问题的默认答案:性能。创建大对象的副本需要时间,只需重用现有对象即可避免。你可以通过将参数声明为const
来禁止修改参数:
1 2 3 4 5 6 7 | int test_by_ref(const int& x) { // this raises a compile time error now // x = 2; // reading is OK return x + 2; } |
相同的行为不仅适用于int
,还适用于所有其他数据类型(string
,vector
)和类对象。使用call-by-reference是一种很好的做法,因为它通常更高效,并且不会通过将它们声明为const
来修改参数。
例如,Google C++样式指南指出:通过引用传递的所有参数必须标记为const
。实际上,Google代码中的一个非常强大的约定是输入参数是值或const引用,而输出参数是指针。
我们现在理解值和const引用,所以让我们来谈谈Google对输出参数是指针的含义。
指针
指针是存储另一个变量的内存地址的变量。它们在C中被大量使用,因为它没有按引用调用。相反,在C中,你定义一个指针变量,该变量存储参数的内存位置,然后按值传递此指针变量。
我们来看看如何在C中编写测试函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | int number = 0; // in C++ void test_by_ref(int& x) { x = 2; } test_by_ref(number); // in C void test_by_pointer(int* px) { // remember the value of px is the address of x // to get the actual value of x we need to dereference the pointer by using `*` *x = 2; } // pointers are defined by <type>* // you get the address of a variable by using & int* pnumber = &number; test_by_pointer(pnumber); // or without intermediate pointer variable test_by_pointer(&number); |
指向int
变量的指针定义为int *
,指向字符串的指针将定义为string *
。 但是,任何指针的实际大小都是相同的:sizeof(int *)== sizeof(string *)
。直观地,指针可以被视为32位或64位,具体取决于平台,unsigned int
变量,其值是另一个变量的内存地址。
为什么我们需要指定指针的类型(int *
或string *
)呢?
这是个好问题。尝试访问指针指向的变量的值时,类型变得很重要。在我们的例子中,要从int* pnumber
指针获取number
的值,我们需要取消引用指针。这是由指针变量上的运算符完成的:int numberValue = pnumber为了知道指针应该读取多少字节,我们需要定义指针的类型。
例如,你可以通过执行以下操作逐字节读取整数变量:
1 2 3 4 5 6 7 | uint32_t number = 0x01020304; // we need to cast it to uint8_t* because &number is of type unit32_t* // remember the pointers all have the same range as they all store memory addresses uint8_t* p = (uint8_t*)(&number); for(int i = 0; i < 4; i++) { std::cout << "Byte " << i << ": " << std::to_string(*(p+i)) << "\n"; } |
如你所见,我们可以对指针进行计算。这称为指针算术。这里,*(p+i)
表示在p
的内存位置向前移动i
指针类型(sizeof(uint8_t))
的大小,并读取uint8_t
。
我们还需要C++中的指针吗?
尽管在C++中使用更简单的引用references
可以做很多事情,但是在使用迭代器或输出参数时,你仍会经常遇到指针。输出参数类似于函数的返回值return
,除了它们作为指针参数传递,然后在函数中修改引用的对象。我们来看一个例子:
1 2 3 4 5 6 7 8 9 | void split(const std::string &name, std::string *first, std::string *last) { std::size_t pos = name.find(" "); *first = name.substr(0, pos); *last = name.substr(pos + 1); } std::string name = "Dan Larimer"; std::string first, last; split(name, &first, &last); |
这里的first
和last
是指针输出参数,包含分割功能完成后的计算结果。输出参数通常用于实际返回参数(string split(...){... return <string>})
,当你需要返回多个值时,就像我们的情况中的两个string
一样。
难道我们不能通过使用引用而不是指针作为输出参数来实现相同的目标吗?
是的,我们可以重写函数来使用引用输出参数:
1 2 3 4 5 6 7 8 9 | void split(const std::string &name, std::string& first, std::string& last) { std::size_t pos = name.find(" "); first = name.substr(0, pos); last = name.substr(pos + 1); } std::string name = "Dan Larimer"; std::string first, last; split(name, first, last); |
你喜欢什么取决于你,归结为个人风格。Google C ++ Styleguide更喜欢将指针作为输出参数的一个原因是因为它在调用者角度看可以清楚地表明该参数可能会发生变化。
然而,能够阅读和理解引用和指针是很重要的。
你现在可能会被许多不同的方法传递给函数。对于刚接触C++的开发人员而言,这通常是最大的学习经验,所以不要担心。在某些时候,你会看到常见的重复模式。
======================================================================
分享一个交互式的在线编程实战,EOS智能合约与DApp开发入门:
本课程帮助你快速入门EOS区块链去中心化应用的开发,内容涵盖EOS工具链、账户与钱包、发行代币、智能合约开发与部署、使用代码与智能合约交互等核心知识点,最后综合运用各知识点完成一个便签DApp的开发。
- java比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Java代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Java工程师不可多得的比特币开发学习课程。
- java以太坊开发教程,主要是针对java和android程序员进行区块链以太坊开发的web3j详解。
- php比特币开发教程,本课程面向初学者,内容即涵盖比特币的核心概念,例如区块链存储、去中心化共识机制、密钥与脚本、交易与UTXO等,同时也详细讲解如何在Php代码中集成比特币支持功能,例如创建地址、管理钱包、构造裸交易等,是Php工程师不可多得的比特币开发学习课程。
- php以太坊,主要是介绍使用php进行智能合约开发交互,进行账号创建、交易、转账、代币开发以及过滤器和交易等内容。
- 以太坊入门教程,主要介绍智能合约与dapp应用开发,适合入门。
- 以太坊开发进阶教程,主要是介绍使用node.js、mongodb、区块链、ipfs实现去中心化电商DApp实战,适合进阶。
- python以太坊,主要是针对python工程师使用web3.py进行区块链以太坊开发的详解。
- C#以太坊,主要讲解如何使用C#开发基于.Net的以太坊应用,包括账户管理、状态与交易、智能合约开发与交互、过滤器和交易等。