《C++ Primer》 Part I(The Basics)
带命令行参数的 C++ 程序:
#include <iostream> #include <stdlib.h> int main(int argc,char* argv[]) { std::cout<<"argv[0]:"<<argv[0]<<std::endl; if(argc > 1) { std::cout<<"argv[1]:"<<atoi(argv[1])<<std::endl; } }
获取键盘输入:
int sum = 0,input; while(cin>>input) { sum += input; } cout<<"sum:"<<sum<<endl;
此循环当遇到 eof 或错误格式输入时会终止,eof 即 end-of-file ,跟操作系统有关,windows 下一般为 ctrl+z,unix 下一般为 ctrl+d
a word = 4 bytes = 32 bits
word,与系统硬件(总线、cpu命令字位数等)有关,如数据总线为16位,则1word为2byte。32位 1word为4byte。
和其他整型不同,char 有三种不同的类型:plain char 、unsigned char 和 signed char。虽然 char 有三种不同的类型,但只有两种表示方式。可以使用 unsigned char 或 signed char 表示 char 类型。使用哪种 char 表示方式由编译器而定。
对于 unsigned 类型来说,其取值范围从 0 到 255(包括 255)。如果赋给超出这个范围的值,那么编译器将会取该值对 256 求模后的值。如赋值336,则实际赋值为80;如赋值为-1,则实际赋值为255。所以在适宜使用unsigned的地方,使用 unsigned 类型比较明智,可以避免值越界导致结果为负数的可能性。
事实上,有些机器上,double 类型比 float 类型的计算要快得多。long double 类型提供的精度通常没有必要,而且还需要承担额外的运行代价。
没有 short 类型的字面值常量。
浮点数字面值,加上 L 或者 l 表示扩展精度。
在字符字面值前加 L 就能够得到 wchar_t 类型的宽字符字面值。
为了兼容 C 语言,C++ 中所有的字符串字面值都由编译器自动在末尾添加一个空字符。
也存在宽字符串字面值,一样在前面加“L”,如 L"a wide string literal" 宽字符串字面值是一串常量宽字符,同样以一个宽空字符结束。
静态类型语言是指在编译时变量的数据类型即可确定的语言,多数静态类型语言要求在使用变量之前必须声明数据类型,某些具有类型推导能力的现代语言可能能够部分减轻这个要求. 如 C\C++\java\C# 等
动态类型语言是在运行时确定数据类型的语言。变量使用之前不需要类型声明,通常变量的类型是被赋值的那个值的类型。 如 javascript\php\ruby\python 等
左值(发音为 ell-value):左值可以出现在赋值语句的左边或右边。
右值(发音为 are-value):右值只能出现在赋值的右边,不能出现在赋值语句的左边。
简单来说就是,左值相当于地址值,右值相当于数据值。
C++ Keywords:
asm | do | if | return | try | auto | double | inline | short |
typedef | bool | dynamic_cast | int | signed | typeid | break | else | long |
sizeof | typename | case | enum | mutable | static | union | catch | explicit |
namespace | static_cast | unsigned | char | export | new | struct | using | class |
extern | operator | switch | virtual | const | false | private | template | void |
const_cast | float | protected | this | volatile | continue | for | public | throw |
wchar_t | default | friend | register | true | while | delete | goto | reinterpret_case |
C++ Operator Alternative Names
and | bitand | compl | not_eq | or_eq | xor_eq |
and_eq | bitor | not | or | xor |
C++ 支持两种初始化变量的形式:复制初始化和直接初始化。
int val2; // uninitialized int ival(1024); // direct-initialization int ival = 1024; // copy-initialization
当初始化类类型对象时,复制初始化和直接初始化之间的差别是很微妙的。对内置类型来说,复制初始化和直接初始化几乎没有差别。
初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。在 C++ 中初始化和赋值是两种不同的操作。
内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成 0,在函数体里定义的内置类型变量不进行自动初始化。
未初始化变量引起的错误难于发现,永远不要依赖未定义行为。
声明与定义的区别,在于是否存在内存分配。
变量和对象不加 extern 永远是定义,因为有内存占用,但类中的除外。
函数只有函数头的是声明,有函数体是定义.
class Test { string str1; //声明 }; /* *************************** */ int main() {
string str1; //定义
}
C++ 语言中,大多数作用域是用花括号来界定的。一般来说,名字从其声明点开始直到其声明所在的作用域结束处都是可见的。
int main() { int i = 10; cout<<"i:"<<i<<endl; {double i = 3.14;cout<<"i:"<<i<<endl;} cout<<"i:"<<i<<endl; return 0; }
但是像上面这样的程序很可能让人大惑不解。在函数内定义一个与函数可能会用到的全局变量同名的局部变量总是不好的。局部变量最好使用不同的名字。
非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须地指定它为 extern。
a.cpp:
extern const double pi = 3.14; int i = 1; int getadd(int i,int j) { return i+j; }
b.cpp:
#include <iostream> int main() { extern const double pi; extern int i; int getadd(int,int); //或:extern int getadd(int,int); std::cout<<i<<"\n"; std::cout<<pi<<"\n"; std::cout<<getadd(3,4)<<"\n"; return 0; }
编译:
g++ -c a.cpp b.cpp
g++ a.o b.o
引用必须要被初始化,形如 int &ri; 这样的都是错误的。用 const 修饰引用,表示不可通过此引用修改原值了。 引用一旦初始化,就不可以再指向别的对象(但可以把别的对象的右值赋给它)。
int main() { int i = 3; const int j = 5; const int &ri = i ; const int &rj = j; int &rk = j; //error int &rx = 10; //error const int &ry = 10; return 0; }
枚举与结构:待定
private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 相对 private 而言,子类的函数也可以访问。
public: 可以被该类中的函数、其友元函数、子类的函数访问,也可以由该类的对象访问
如果使用 class 关键字来定义类,那么定义在第一个访问标号前的任何成员(数据成员和成员函数)都隐式指定为 private;如果使用 struct 关键字,那么这些成员都是 public。使用 class 还是 struct 关键字来定义类,仅仅影响默认的初始访问级别。
头文件用于声明而不是定义,因为同一个程序中有两个以上文件含有任一个定义都会导致多重定义链接错误。
对于头文件不应该含有定义这一规则,有三个例外。头文件可以定义类、值在编译时就已知道的 const 对象和 inline 函数。这些实体可在多个源文件中定义,只要每个源文件中的定义是相同的。
避免多重包含:
#ifndef SALESITEM_H
#define SALESITEM_H
// 正文
#endif
或者可以在头文件前面加上:#pragma once
int main() { string word; while (cin >> word) cout << word << endl; return 0; } //如果输入 Hello World!,最终只会显示 Hello
int main() { string line; while (getline(cin, line)) cout << line << endl; return 0; } //如果输入Hello World! 此时就能完整显示了。 //getline(cin,line) 返回的值是 void,同时也说明了while(void)是死循环。
获取变量类型:
int i = 12; cout<<typeid(i).name()<<endl;
string 对象和字符串字面值是不一样的,允许把string对象和字符串字面值直接相加(并返回一个字符串对象),但不允许把两个字符串字面值相加,形如 string str = "Hello" + " World!"; 是错误的。
//使用 size_type 类型时,必须指出该类型是在哪里定义的。vector 类型总是包括 vector 的元素类型: vector<int>::size_type // ok vector::size_type // error
//初始化10个元素为0的vector,可以赋值 vector<int> vect(10); for(vector<int>::size_type i = 0; i != vect.size(); i++) vect[i] = i; //这是一个空vector,不能直接赋值,可以通过 push_back 来添加元素 vector<int> vect1;
习惯于 C 编程的程序员可能会觉得难以理解,for 循环的判断条件用 != 而不是用 < 来测试 vector 下标值是否越界;以及没有在 for 循环之前就调用 size 成员函数并保存其返回的值,而是在 for 语句头中调用 size 成员函数。
使用 != 学习完泛型编程后,你将会明白这种习惯的合理性。
有些数据结构(如 vector)可以动态增长。循环可以容易地增加新元素,如果确实增加了新元素的话,那么测试已保存的 size 值作为循环的结束条件就会有问题,因为没有将新加入的元素计算在内。像 size 这样的小库函数几乎都定义为内联函数,所以每次循环过程中调用它的运行时代价是比较小的。
必须是已存在的元素才能用下标操作符进行索引,也就说明了,不能通过下标操作来增加vector的元素。
vector<int> vect(10); for(vector<int>::const_iterator it = vect.begin();it != vect.end(); it++) *it = 3; //ERROR,it's const_iterator. const vector<int>::iterator it1 = vect.begin(); //这种方式在实际中几乎不会使用,因为迭代器就是为迭代而生的 *it1 = 3; //OK it1++; //ERROR
任何改变 vector 长度的操作都会使已存在的迭代器失效。例如,在调用 push_back 之后,就不能再信赖指向 vector 的迭代器的值了。
iter1 - iter2 用来计算两个迭代器对象的距离,该距离是名为 difference_type 的 signed 类型 size_type 的值,这里的 difference_type 是 signed 类型,因为减法运算可能产生负数的结果。
数组的维数必须用值大于等于1的常量表达式定义,此常量表达式只能包含整型字面值常量、枚举常量或者用常量表达式初始化的整型 const 对象。
int size1 = 3; const size2 = 3; int array1[size1] = {1,2,3}; //ERROR int array2[size2] = {1,2,3}; //OK
如果没有显式提供元素初值,在函数体外定义的内置类型数组,其元素均初始化为 0。在函数体内定义的内置类型数组,其元素无初始化。
不管数组在哪里定义,如果其元素为类类型,则自动调用该类的默认构造函数进行初始化;如果该类没有默认构造函数,则必须为该数组的元素提供显式初始化。
显式初始化的数组不需要指定数组的维数值,编译器会根据列出的元素个数来确定数组的长度:
int ia[] = {0, 1, 2}; // an array of dimension 3
如果指定了数组维数,那么初始化列表提供的元素个数不能超过维数值。如果维数大于列出的元素初值个数,则只初始化前面的数组元素;剩下的其他元素,若是内置类型则初始化为0,若是类类型则调用该类的默认构造函数进行初始化:
字符数组既可以用一组由花括号括起来、逗号隔开的字符字面值进行初始化,也可以用一个字符串字面值进行初始化。然而,要注意这两种初始化形式并不完全相同,字符串字面值包含一个额外的空字符(null)用于结束字符串。当使用字符串字面值来初始化创建的新数组时,将在新数组中加入空字符'\0'
const char ch3[6] = "Daniel"; // error: Daniel is 7 elements
//下列数组的值是什么? string sa[10]; int ia[10]; int main()}
{ string sa2[10]; int ia2[10];
在用下标访问元素时,vector 使用 vector::size_type 作为下标的类型,而数组下标的正确类型则是 size_t
对大多数的编译器来说,如果使用未初始化的指针,会将指针中存放的不确定值视为地址,然后操纵该内存地址中存放的位内容。使用未初始化的指针相当于操纵这个不确定地址中存储的基础数据。因此,在对未初始化的指针进行解引用时,通常会导致程序崩溃。
C++ 语言无法检测指针是否未被初始化,也无法区分有效地址和由指针分配到的存储空间中存放的二进制位形成的地址。建议程序员在使用之前初始化所有的变量,尤其是指针。
如果必须分开定义指针和其所指向的对象,则将指针初始化为 0。因为编译器可检测出 0 值的指针,程序可判断该指针并未指向一个对象。
// cstdlib #defines NULL to 0 int *pi = NULL; // ok: equivalent to int *pi = 0;
C++ 提供了一种特殊的指针类型 void*,它可以保存任何类型对象的地址:
int i = 32; void *pi = &i; cout<<*(int *)pi<<endl;
void* 指针只支持几种有限的操作:与另一个指针进行比较;向函数传递 void* 指针或从函数返回 void* 指针;给另一个 void* 指针赋值。void型指针,表示这个指针指向的内存中的数据的类型要由用户来指定。比如内存分配函数malloc函数返回的指针就是void *型,用户在使用这个指针的时候,要进行强制类型转换,也就是显式说明该指针指向的内存中是存放的什么类型的数据(int *)malloc(1024)表示强制规定malloc返回的void*指针指向的内存中存放的是一个个的int型数据。
虽然使用引用(reference)和指针都可间接访问另一个值,但它们之间有两个重要区别。第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。第二个重要区别则是赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用与另一个对象关联。引用一经初始化,就始终指向同一个特定对象(这就是为什么引用必须在定义时初始化的原因)。
指针一般占4个字节。
两个指针减法操作的结果是标准库类型(library type)ptrdiff_t 的数据。与 size_t 类型一样,ptrdiff_t 也是一种与机器相关的类型,在 cstddef 头文件中定义。size_t 是 unsigned 类型,而 ptrdiff_t 则是 signed 整型。
这两种类型的差别体现了它们各自的用途:size_t 类型用于指明数组长度,它必须是一个正数;ptrdiff_t 类型则应保证足以存放同一数组中两个指针之间的差距,它有可能是负数。
遍历数组:
int ia[] = {1,2,3,4}; for(int *p = ia,pend = ia+sizeof(ia)/sizeof(ia[0]);p != pend; p++) { cout<<*p<<endl; } for(int i = 0,size = sizeof(ia)/sizeof(ia[0]); i != size ;i++) { cout<<*(ia+i)<<endl; }
指向 const 对象的指针,也必须具有 const 特性:
const double *cptr; // cptr may point to a double that is const
允许把非 const 对象的地址赋给指向 const 对象的指针
int i = 10; const int *pi = &i; i = 20; cout<<*pi<<endl; //20
int *const pi2 = &i; //const指针必须在定义时初始化。
下面这个问题相当绕,晕~~
typedef string *pstring; string str = "Hello"; string str2 = "Hello2"; const pstring s = &str; //相当于: string *const s; *s = "Hi"; s = &str2; //ERROR! cout<<str<<endl;
字符串字面值的类型是字符常量的数组,就是 const char 类型的数组。C++ 从 C 语言继承下来的一种通用结构是C 风格字符串,而字符串字面值就是该类型的实例。
char c1[] = "Hello"; char c2[] = {'a','b','\0'}; string c3 = "haha"; cout<<typeid(c1).name()<<endl; //A6_c cout<<typeid(c2).name()<<endl; //A3_c cout<<typeid("haha").name()<<endl; //A5_c cout<<typeid(c3).name()<<endl; //Ss
字符类型的指针与其它基本类型的指针之间有一个非常重要的区别,如int数组指针,输出的是一个地址,而字符数组指针输出的是字符输出,直至遇到'\0'为止
char ca[] = "Hello"; char *c = ca; //跟int数组是一样的,ca表示字符数组的第一个字符的指针
cout<<*ca<<endl; //H 这里输出*a也是一样的 cout<<ca<<endl; //Hello 这里输出a也是一样的
string 与 char* 转转:
char *c = "Hello"; string str(c); //这里用: string str = c; 也可以
cout<<str<<endl; //Hello const char *cc = str.c_str(); cout<<cc<<endl; //Hello
C风格字符串遍历:
const char *c = "Hello"; while(*c) { cout<<*c<<endl; c++; }
对大部分的应用而言,使用标准库类型 string,除了增强安全性外,效率也提高了,因此应该尽量避免使用 C 风格字符串。
C 语言程序使用一对标准库函数 malloc 和 free 在自由存储区中分配存储空间,而 C++ 语言则使用 new 和 delete 表达式实现相同的功能。
动态分配数组时,只需指定类型和数组长度,不必为数组对象命名,new表达式返回指向新分配数组的第一个元素的指针:
int *pia = new int[10]; // array of 10 uninitialized ints
在自由存储区中创建的数组对象是没有名字的,程序员只能通过其地址间接地访问堆中的对象。
string *psa = new string[10]; // array of 10 empty strings int *pia = new int[10]; // array of 10 uninitialized ints
第一个数组是 string 类型,分配了保存对象的内存空间后,将调用 string 类型的默认构造函数依次初始化数组中的每个元素。
第二个数组则具有内置类型的元素,分配了存储 10 个 int 对象的内存空间,但这些元素没有初始化。
也可使用跟在数组长度后面的一对空圆括号,对数组元素做值初始化 int *pia2 = new int[10] (); // array of 10 uninitialized ints
对于动态分配的数组,其元素只能初始化为元素类型的默认值,而不能像数组变量一样,用初始化列表为数组元素提供各不相同的初值。const对象的动态数组,必须提供初始化,但这样的数组由于不能更改元素,实际用处不大。
之所以要动态分配数组,往往是由于编译时并不知道数组的长度。我们可以编写如下代码:
size_t n = get_size(); // get_size returns number of elements needed int* p = new int[n]; for (int* q = p; q != p + n; ++q) // 即使这里 n == 0,也不用担心,因为循环一次都不会执行 /* process the array */ ;
C++ 虽然不允许定义长度为 0 的数组变量,但明确指出,调用 new 动态创建长度为 0 的数组是合法的。
int ia[0]; //Error int *pi = new int[0]; //OK
动态分配的内存最后必须进行释放,C++ 语言为指针提供 delete [] 表达式释放指针所指向的数组空间。 delete [] pi;
回收数组时缺少空方括号对,至少会导致运行时少释放了内存空间,从而产生内存泄漏(memory leak)。对于某些系统和/或元素类型,有可能会带来更严重的运行时错误。因此,在释放动态数组时千万别忘了方括号对。
通常,由于 C 风格字符串与字符串字面值具有相同的数据类型,而且都是以空字符 null 结束,因此可以把 C 风格字符串用在任何可以使用字符串字面值的地方。
用数组初始化vector对象:
int ia[5] = {0,1,2,3,4}; vector<int> vect(ia,ia+5); //0,1,2,3,4 vector<int> vect1(ia,ia+3); //0,1,2
严格地说,C++ 中没有多维数组,通常所指的多维数组其实就是数组的数组:
int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; //等价于: int ia[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; int ia[3][4] = {{1},{5},{9}}; //只初始化每行第一个元素,其它为默认值0 int ia[3][4] = {1,2,3,4}; //只初始化第一行的四个元素,其它为默认值0 int (*pia)[4] = ia; //这里的数组名ia表示这个二维数组的“下级元素”,即子数组。所以 pia 其实感觉相当于 ia吧 cout<<(*(pia+1))[1]<<endl; //输出是6 //typedef 类型定义(第 2.6 节)可使指向多维数组元素的指针更容易读、写和理解。以下程序用 typedef 为 ia 的元素类型定义新的类型名: typedef int int_array[4]; int_array *ip = ia;
Bitwise Operators:
~ | bitwise NOT(位求反) |
~expr |
<< | left shift(左移) |
expr1 << expr2 |
>> | right shift(右移) |
expr1 >> expr2 |
& |
bitwise AND(位与) |
expr1 & expr2 |
^ | bitwise XOR(位异或) |
expr1 ^ expr2 |
| | bitwise OR(位或) | expr1 | expr2 |
对于位操作符,由于系统不能确保如何处理其操作数的符号位,所以强烈建议使用unsigned整型操作数。
复合赋值操作符(10个):
+= -= *= /= %= // arithmetic operators <<= >>= &= ^= |= // bitwise operators
建议,只有在必要的时候,才使用后置操作符。因为前置操作需要做的工作更少,只需加 1 后返回加 1 后的结果即可。而后置操作符则必须先保存操作数原来的值,以便返回未加 1 之前的值作为操作的结果。对于 int 型对象和指针,编译器可优化掉这项额外工作。但是对于更多的复杂迭代器类型,这种额外工作可能会花费更大的代价。因此,养成使用前置操作这个好习惯,就不必操心性能差异的问题。
vector<int>::iterator iter = ivec.begin(); // prints 10 9 8 ... 1 while (iter != ivec.end()) cout << *iter++ << endl; // iterator postfix increment
由于后自增操作的优先级高于解引用操作,因此 *iter++ 等效于 *(iter++)
而不采用下面这种冗长的等效代码:
cout << *iter << endl; ++iter;
C++ 语言为包含点操作符和解引用操作符的表达式提供了一个同义词:箭头操作符(->)。
条件操作符是 C++ 中唯一的三元操作符,它允许将简单的 if-else 判断语句嵌入表达式中。 cond ? expr1 : expr2;
int *pi = new int; // pi points to an uninitialized int int *pi = new int(); // pi points to an int value-initialized to 0 int *pi = new int(3); // pi points to an int value-initialized to 3
如果指针指向不是用 new 分配的内存地址,则在该指针上使用 delete 是不合法的。
如果指针的值为 0,则在其上做 delete 操作是合法的,但这样做没有任何意义:
int *ip = 0;
delete ip; // ok: always ok to delete a pointer that is equal to 0
C++ 保证:删除 0 值的指针是安全的。
删除指针后,该指针变成悬垂指针。悬垂指针指向曾经存放对象的内存,但该对象已经不再存在了。悬垂指针往往导致程序错误,而且很难检测出来。
一旦删除了指针所指向的对象,立即将指针置为 0,这样就非常清楚地表明指针不再指向任何对象。
虽然有时候确实需要强制类型转换,但是它们本质上是非常危险的。
static_cast dynamic_cast const_cast reinterpret_cast
double d = 3.14; cout<<static_cast<int>(d)<<endl; //3 cout<<(int)d<<endl; //3 旧式强制类型转换,但不推荐使用 void *pd = &d; double *p = static_cast<double*>(pd); //还原void*指针
强制类型转换关闭或挂起了正常的类型检查,强烈建议程序员避免使用强制类型转换,不依赖强制类型转换也能写出很好的 C++ 程序。
有些编程风格建议总是在 if 后面使用花括号。这样做可以避免日后修改代码时产生混乱和错误。比如悬垂 else 问题:
if (minVal <= ivec[i]) if (minVal == ivec[i]) ++occurs; else { // this else goes with the inner if, not the outer one! 与我们缩进的意图不同 minVal = ivec[i]; occurs = 1; } //改成下面即可: if (minVal <= ivec[i]) if (minVal == ivec[i]) ++occurs; else { // this else goes with the inner if, not the outer one! minVal = ivec[i]; occurs = 1; }
哪怕没有语句要在 default 标号下执行,定义 default 标号仍然是有用的。定义 default 标号是为了告诉它的读者,表明这种情况已经考虑到了,只是没什么要执行的。
switch (ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++i; break; default: break }
case标号必须是整型常量表达式(int,char,bool等),如果两个 case 标号具有相同的值,同样也会导致编译时的错误。
面将一个数组的内容复制到另一个数组:
// arr1 is an array of ints int *source = arr1; size_t sz = sizeof(arr1)/sizeof(*arr1); // number of elements int *dest = new int[sz]; // uninitialized elements while (source != arr1 + sz) *dest++ = *source++; // copy element and increment pointers //*dest++ = *source++; 是下面的简洁写法,而且很流行哦 *dest = *source; // copy element ++dest; // increment the pointers ++source;
break 语句用于结束最近的 while、do while、for 或 switch 语句,并将程序的执行权传递给紧接在被终止语句之后的语句。
从上世纪 60 年代后期开始,不主张使用 goto 语句。goto 语句使跟踪程序控制流程变得很困难,并且使程序难以理解,也难以修改。所有使用 goto 的程序都可以改写为不用 goto 语句,因此也就没有必要使用 goto 语句了。
int i = 0; loop: cout<<++i<<endl; if(i != 10) goto loop;
最大公约数算法:
//求最大公约数 int gcd(int v1, int v2) { while (v2) { int temp = v2; v2 = v1 % v2; v1 = temp; } return v1; } // recursive version greatest common divisor program int rgcd(int v1, int v2) { if (v2 != 0) // we're done once v2 gets to zero return rgcd(v2, v1%v2); // recurse, reducing v2 on each call return v1; }
函数不能返回另一个函数或者内置数组类型,但可以返回指向函数的指针,或指向数组元素的指针的指针。
在 C++ 标准化之前,如果缺少显式返回类型,函数的返回值将被假定为 int 型。
没有任何形参的函数可以用空形参表或含有单个关键字 void 的形参表来表示。如 void fun(){/*...*/} void fun(void){/*...*/}
//通过指针交换两个值。传入的实参即两个地址值并没有被改变,交换的只是传入地址所对应的值(可以通过解引地址<的副本>来改变对应的值) void swap(int* a,int* b) { int temp; temp = *a; *a = *b; *b = temp; } //通过引用交换两个值。传入的引用即两个别名并没有被改变,交换的只是传入引用所对应的值(可以通过别名<的副本>来改变对应的值) void swap_with_reference(int& a,int& b){ int temp; temp = a; a = b; b = temp; }
复制实参的局限性:
1、当需要改变实参的值时;
2、当对象非常大时,复制对象会付出过多的时间和存储空间代价。使用引用形参,函数可以直接访问实参对象,而无须复制它。
3、当没有办法复制对象时。
对于上面的情况,有效的办法是将形参定义为引用或指针类型。因为复制指针或引用消耗很小,另外通过指针或引用都可以改变所指向的对象。
另外,可以使用引用形参来返回非函数返回值之外的其它数据。
如果不希望通过引用形参修改值时,比如说避免复制大对象时的只读操作,可以声明 const 引用形参,用 const 修饰引用变量,可使其不能再改变。
指针常量和常量指针是两种概念: const int *i;<不能通过指针来改变原值> int* const i;(int const *i) <不能用 i 再来表示其它对象的指针 >
但因为别名指向某个对象,是定义好之后就不能改变的。所以不需要 int const &i 类似于指针的这层意义,而等价于 const int &i ,表示不能通过该别名来修改原值。
如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为 const 引用。
应该将不需要修改的引用形参定义为 const 引用。普通的非 const 引用形参在使用时不太灵活。这样的形参既不能用 const 对象初始化,也不能用字面值或产生右值的表达式实参初始化。
//交换两个指针指向的值 void pointswap(int *&a, int *&b) { int* temp; temp = a; a = b; b = temp; } //int *&a 表示指针的别名,不要弄反了。 //调用时: int x = 10; int y = 20; int* px = &x; int* py = &y; pointswap(px,py); //pointswap(&x,&y); //error! why? cout<<"*px:"<<*px<<endl<<"*py:"<<*py<<endl;
从避免复制 vector 的角度出发,应考虑将形参声明为引用类型。然而事实上,C++ 程序员倾向于通过传递指向容器中需要处理的元素的迭代器来传递容器:
如void print(vector<int>::const_iterator beg,vector<int>::const_iterator end){/*...*/}
数组形参的定义:
// three equivalent definitions of printValues void printValues(int*) { /* ... */ } void printValues(int[]) { /* ... */ } void printValues(int[10]) { /* ... */ }
虽然不能直接传递数组,但是函数的形参可以写成数组的形式。虽然形参表示方式不同,但可将使用数组语法定义的形参看作指向数组元素类型的指针。上面的三种定义是等价的,形参类型都是 int*。
通常,将数组形参直接定义为指针要比使用数组语法定义更好。这样就明确地表示,函数操纵的是指向数组元素的指针,而不是数组本身。由于忽略了数组长度,形参定义中如果包含了数组长度则特别容易引起误解。
不需要修改数组形参的元素时,函数应该将形参定义为指向 const 对象的指针:
// f won't change the elements in the array
void f(const int*) { /* ... */ }
和其他数组一样,多维数组以指向 0 号元素的指针方式传递。多维数组的元素本身就是数组。除了第一维以外的所有维的长度都是元素类型的一部分,必须明确指定:
// first parameter is an array whose elements are arrays of 10 ints void printValues(int (matrix*)[10], int rowSize);
上面的语句将 matrix 声明为指向含有 10 个 int 型元素的数组的指针。*matrix 两边的圆括号是必需的。
有三种常见的编程技巧确保函数的操作不超出数组实参的边界。
1、在数组本身放置一个标记来检测数组的结束。类似于C风格字符串。
2、形参中传递起始元素指针和最后一个元素的指针。类似于迭代器风格。
3、显式传递表示数组大小的形参。int main(int argc, char *argv[]) { ... } 就是这种形式。这种用法在 C 程序和标准化之前的 C++ 程序中十分普遍。
关于可变形参,如:
void fun(int i,...);
void fun(...);
待整理。
函数声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但是不必对形参命名。这三个元素被称为函数原型描述了函数的接口。
函数调用的实参按位置解析,默认实参只能用来替换函数调用缺少的尾部实参。
设计带有默认实参的函数,其中部分工作就是排列形参,使最少使用默认实参的形参排在最前,最可能使用默认实参的形参排在最后。
通常,应在函数声明中指定默认实参,并将该声明放在合适的头文件中。如 int ff(int = 0);
如果在函数定义的形参表中提供默认实参,那么只有在包含该函数定义的源文件中调用该函数时,默认实参才是有效的。
通常,应在函数声明中指定默认实参,并将该声明放在合适的头文件中。
// static 局部对象
size_t count_calls() { static size_t ctr = 0; // value will persist across calls return ++ctr; } int main() { for (size_t i = 0; i != 10; ++i) cout << count_calls() << endl; return 0; }
inline 说明对于编译器来说只是一个建议,编译器可以选择忽略这个。
大多数的编译器都不支持递归函数的内联。一个 1200 行的函数也不太可能在调用点内联展开。
内联函数应该在头文件中定义,这一点不同于其他函数。
const 成员函数 又称为 常量成员函数。
如果一个类为 Test,有一个对象为 test ,一个成员函数为 bool isok(int);
则 test.isok(3); 会被编译器这样重写:
Test::isok(&test, 3);
const 成员函数改变了隐含的 this 形参的类型,const 成员函数不能修改调用该函数的对象。
const 对象、指向 const 对象的指针或引用只能用于调用其 const 成员函数,如果尝试用它们来调用非 const 成员函数,则是错误的。
仅当形参是引用或指针时,形参是否为 const 才有影响。
构造函数要定义成 public ,如果构造函数定义成 private ,那这个构造函数就没有用了。
指向函数的指针——待整理。
标准库类型不允许做复制或赋值操作。只有支持复制的元素类型可以存储在 vector 或其他容器类型里。由于流对象不能复制,因此不能存储在 vector(或其他)容器中。
形参或返回类型也不能为流类型。如果需要传递或返回 IO 对象,则必须传递或返回指向该对象的指针或引用。
8.1节~8.5节:待整理。