Hi!这里是山幺幺的c++ primer系列。写这个系列的初衷是,虽然在学校学习了c++,但总觉得对这门语言了解不深,因此我想来啃啃著名的c++ primer,并在这里同步记录我的学习笔记。由于我啃的是英文版,所以笔记是中英文夹杂的那种。另外由于已有一定的编程基础,所以这个系列不会含有太基础的知识,想入门的朋友最好自己啃书嘻嘻~
关于参数
参数赋值顺序
- 我们只知道哪个实参对应哪个形参,the compiler is free to evaluate the arguments in whatever order it prefers.
参数类型转换
- 和初始化的类型转换相似,比如:对于 char fact(int x) 来说,fact(3.14) 等价于 fact(3)
无形参时
- char fact() 或者 char fact(void)
关于返回类型
- 返回类型不能是 array 或 function
关于局部变量
作用域
- They are “local” to that function and hide declarations of the same name made in an outer scope.
生存期
- 定义在任何函数外的对象:exist throughout the program’s execution
- 局部变量:depends on how it is defined
局部变量的分类
- Automatic Objects:Objects that exist only while a block is executing
- 栗子:形参(parameter)
- 若未被初始化,Automatic Objects 会被 default initialize,因此,uninitialized local variables of built-in type have undefined values
- Local static Objects:initialized before the first time execution passes through the object’s definition;生存期持续到程序终止
- 若未被初始化,Local static Objects 会被 value initialize,因此,local statics of built-in type are initialized to zero
- 栗子:会打印1~10
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; }
关于函数声明
基本性质
- 声明之后才能使用函数
- 函数只能定义一次,但可以声明多次
- 函数声明中可以不写形参名字(但为了可读性通常会写)
栗子
void print(vector<int>::const_iterator beg,
vector<int>::const_iterator end);
声明与定义
- 和变量一样,函数在头文件中声明,在source file中定义
- The source file that defines a function should include the header that contains that function’s declaration. That way the compiler will verify that the definition and declaration are consistent.
关于Separate Compilation
概述
- Separate compilation lets us split our programs into several files, each of which can be compiled independently.
- 栗子:factMain.cc中的main函数调用了fact.cc中定义的fact函数,CC是编译器的名字;.o是object file,内含object code;The compiler lets us link object files together to form an executable
# 以下为一起编译的情形 $ CC factMain.cc fact.cc # generates factMain.exe or a.out $ CC factMain.cc fact.cc -o main # generates main or main.exe # 以下为独立编译的情形 $ CC -c factMain.cc # generates factMain.o $ CC -c fact.cc # generates fact.o $ CC factMain.o fact.o # generates factMain.exe or a.out $ CC factMain.o fact.o -o main # generates main or main.exe
关于参数传递
基本性质
- 若形参是一个引用,则它与实参(argument)绑定;否则,实参的值被复制并用于初始化形参
Passing Arguments by Value
- 修改形参的值不会影响实参,哪怕它是指针;不过可以通过指针修改它指向的对象
- 调用拷贝构造函数,把实参的值拷贝给形参
void reset(int *ip)
{
*ip = 0; // changes the value of the object to which ip points
ip = 0; // changes only the local copy of ip; the argument is unchanged
}
Passing Arguments by Reference
- 直接调用构造函数,构造形参
- Using References to Avoid Copies:避免复制带来的浪费,而且some class types (including the IO types) cannot be copied
- Using Reference Parameters to Return Additional Information:通过引用隐式返回需要的值,比如:
string::size_type find_char(const string &s, char c,
string::size_type &occurs)
{
auto ret = s.size(); // position of the first occurrence, if any
occurs = 0; // set the occurrence count parameter
for (decltype(ret) i = 0; i != s.size(); ++i) {
if (s[i] == c) {
if (ret == s.size())
ret = i; // remember the first occurrence of c
++occurs; // increment the occurrence count
}
}
return ret; // count is returned implicitly in occurs
}
关于用实参初始化形参的规则
top-level const
- 传形参给实参时,是用实参初始化形参,因此top-level const会被忽略
- 栗子
void fcn(const int i) { /* fcn can read but not write to i */ }
void fcn(int i) { /* . . . */ } // error: redefines fcn(int)
尽管这两个函数的形参看起来不一样,但由于top-level const会被忽略,所以we can pass exactly the same types to either version of fcn,所以第二个定义会报错
low-level const
- 和普通变量的初始化一样,用实参初始化形参时low-level const不会被忽略,所以不能用low-level const初始化nonconst
int i = 0;
const int ci = i;
reset(&i); // calls the version of reset that has an int* parameter
reset(&ci); // error: can't initialize an int* from a pointer to a const int object
plain reference
- a plain reference must be initialized from an object of the same type(可以隐式类型转换的也不行);plain reference不能引用字面量
string::size_type ctr = 0;
reset(i); // calls the version of reset that has an int& parameter
reset(ci); // error: can't bind a plain reference to the const object ci
reset(42); // error: can't bind a plain reference to a literal
reset(ctr); // error: types don't match; ctr has an unsigned type
关于用array做形参
基本性质
- 直接用array名做实参,即passing a pointer to the array’s first element
- 因为不能复制array,所以无法通过传值的方法,而是传指针
// 以下函数等价
// each function has a single parameter of type const int*
void print(const int*);
void print(const int[]); // shows the intent that the function takes an array
void print(const int[10]); // 这个10除了增加可读性外没有意义
PS:为什么最后一行的10没有意义?const int [10]的声明编译器自动处理为const int *,所以在函数的形参列表里指定数组的成员个数是没有意义的,传入的实参究竟是比10多还是比10少对于调用函数而言都是合法的
- 也可以使用引用
// ok: parameter is a reference to an array; the dimension is part of the type
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}
// 注意括号是必要的
f(int &arr[10]) // error: declares arr as an array of references
PS:为什么不能定义元素是引用的array?引用没有自身的地址,不占用内存空间,因此,声明引用数组没有办法分配内存空间,因为根本就没有空间可以分配给引用,所以不能声明和定义引用数组
多维array
- 因为多维array是array的array,所以传的是一个指向array的指针
- The size of the second (and any subsequent) dimension is part of the element type and must be specified
// matrix points to the first element in an array whose elements are arrays of ten ints
void print(int (*matrix)[10], int rowSize) { /* . . . */ }
// 等价于(第一维大小写不写无所谓,编译器会无视,自动处理为指向array的指针)
void print(int matrix[][10], int rowSize) { /* . . . */ }
// 注意第一行的括号是必要的
int *matrix[10]; // array of ten pointers
关于main函数的参数
int main(int argc, char *argv[]) { ... } // 等价于
int main(int argc, char **argv) { ... }
- argv:an array of pointers to C-style character strings
- argc:argv中的元素个数
- argv中的第一个元素要么指向程序名字,要么指向空字符串,subsequent elements pass the arguments provided on the command line,最后一个元素是0,栗子:(命令行是 prog -d -o ofile data0)
argv[0] = "prog"; // or argv[0] might point to an empty string
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
关于形参个数可变的函数
initializer_list
- 要求所有参数类型相同
- 是一个array of values of the specified type
- 定义在头文件initializer_list中
- initializer_list中的元素都是const value
- 操作
- 栗子
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << ": ";
for (const auto &elem : il)
cout << elem << " " ;
cout << endl;
}
// expected和actual都是string
if (expected != actual)
error_msg(ErrCode(42), {"functionX", expected, actual});
else
error_msg(ErrCode(0), {"functionX", "okay"});
variadic template
- 参数类型可以不同
- 在16章会讲
Ellipsis Parameters(省略号)
- 最好只在need to interface to C functions的程序中使用
- Ellipsis parameters should be used only for types that are common to both C and C+ + . In particular, objects of most class types are not copied properly when passed to an ellipsis parameter.
- 省略号只能出现在参数list的最后,栗子:
void foo(parm_list, ...);
void foo(...);
No type checking is done for the arguments that correspond to the ellipsis parameter. In this first form, the comma following the parameter declarations is optional.
关于return
不一定非得含有return语句的
- void型函数
- main函数:若无return语句,the compiler implicitly inserts a return of 0
值传递:将返回值拷贝给临时变量,并返回临时变量
- 函数returns a copy of variable(调用拷贝构造函数来初始化临时变量并返回)
引用传递:无拷贝,返回对象本身
- 函数返回值为引用时,返回的是左值;返回值为其他类型时,返回的是右值
char &get_val(string &str, string::size_type ix)
{
return str[ix]; // get_val assumes the given index is valid
}
int main()
{
string s("a value");
cout << s << endl; // prints a value
get_val(s, 0) = 'A'; // changes s[0] to A
cout << s << endl; // prints A value
return 0;
}
不要返回局部变量的引用或指针
const string &manip()
{
string ret;
// transform ret in some way
if (!ret.empty())
return ret; // WRONG: returning a reference to a local object!
else
return "Empty"; // WRONG: "Empty" is a local temporary string
}
返回{...}
- In a function that returns a built-in type, a braced list may contain at most one value, and that value must not require a narrowing conversion
main函数中的return
- A zero return indicates success; most other values indicate failure. A nonzero value has a machine-dependent meaning.
- 头文件cstdlib中定义了如下两个preprocessor variables:
int main()
{
if (some_failure)
return EXIT_FAILURE; // defined in cstdlib
else
return EXIT_SUCCESS; // defined in cstdlib
}
PS:preprocessor variables前不能加std::这种,也不能使用using声明
返回array
- 因为无法复制array,所以函数不能直接返回array,但可以返回a pointer or a reference to an array
- 函数声明
// 法一:使用别名
typedef int arrT[10]; // arrT is a synonym for the type array of ten ints
arrT* func(int i); // func returns a pointer to an array of five ints
// 法二:不使用别名
int (*func(int i))[10]; // 理解:(*func(int))表示可以dereference func的返回结果,[10]表示返回结果是大小为10的array,int表示返回的array元素为int
// 法三:trailing return type
auto func(int i) -> int(*)[10];
// 法四:decltype
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// returns a pointer to an array of five int elements
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // returns a pointer to the array
}
什么时候会产生临时变量
值传递
- 函数返回使用值传递时:将返回值拷贝给临时变量,并返回临时变量
引用传递
- 形参是plain reference时:当实参和形参的类型不匹配会产生临时变量
- 形参是const reference时:当实参和形参的类型匹配,但实参不是左值时,会产生临时变量;当实参和形参的类型不匹配,但是可以转换为正确的类型时,会产生临时变量
类型转换
- 强制类型转换时:会产生临时变量
重载函数
基本性质
- 重载函数:Functions that have the same name but different parameter lists and that appear in the same scope
- must differ in the number or the type(s) of their parameters
- 有无top-level const不影响type difference
// 以下两个函数属于重复declare
Record lookup(Phone);
Record lookup(const Phone); // redeclares Record lookup(Phone)
// 以下两个函数属于重载,对于nonconst对象两个函数都可以使用,不过编译器会prefer用第二个
Record lookup(Account&); // function that takes a reference to Account
Record lookup(const Account&); // new function that takes a const reference
- 类中的const与nonconst成员函数可以重载,因为this指针类型不同
class A
{
public:
void foo(void)
{
cout << "非常函数" << endl;
}
void foo(void)const
{
cout << "常函数" << endl;
}
};
重载和作用域
- if we declare a name in an inner scope, that name hides uses of that name declared in an outer scope,而不是重载outer scope的函数,因为local definition hides outer definition
string read();
void print(const string &);
void print(double); // overloads the print function
void fooBar(int ival)
{
bool read = false; // new scope: hides the outer declaration of read
string s = read(); // error: read is a bool variable, not a function
// bad practice: usually it's a bad idea to declare functions at local scope
void print(int); // new scope: hides previous instances of print
print("Value: "); // error: print(const string &) is hidden
print(ival); // ok: print(int) is visible
print(3.14); // ok: calls print(int); print(double) is hidden
}
重载函数的匹配
- 第一步:找candidate functions:函数名相同 + visible declaration
- 第二步:找viable functions:参数个数相同 + 参数类型匹配(convertible也可以)
- 第三步:找best match:
- The match for each argument is no worse than the match required by any other viable function
- There is at least one argument for which the match is better than the match provided by any other viable function
- 若没有best match,则编译器会报错:the function call is ambiguous
- 附:参数类型转换排序(用于决定谁是best match)
- exact match:① argument and parameter types are identical;② argument is converted from an array or function type to the corresponding pointer type;③ top-level const is added to or discarded from the argument
- match through a const conversion
Record lookup(const Account&); Account b; lookup(b);
- match through a promotion
- match through an arithmetic or pointer conversion
- match through a class-type conversion(第14章会讲)
参数类型转换
- small integral types always promote to int or to a larger integral type
- all the arithmetic conversions are treated as equivalent to each other
void manip(long);
void manip(float);
manip(3.14); // error: ambiguous call
默认参数
默认参数位序
- if a parameter has a default argument, all the parameters that follow it must also have default arguments
- 排序原则:those least likely to use a default value appear first and those most likely to use a default appear last
调用含默认参数的函数
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
string window;
window = screen(); // equivalent to screen(24,80,' ')
window = screen(66);// equivalent to screen(66,80,' ')
window = screen(66, 256); // screen(66,256,' ')
window = screen(66, 256, '#'); // screen(66,256,'#')
window = screen(, , '?'); // error: can omit only trailing arguments
含默认参数的函数的声明
- any subsequent declaration can add a default only for a parameter that has not previously had a default specified
- defaults can be specified only if all parameters to the right already have defaults
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // error: redeclaration
string screen(sz = 24, sz = 80, char); // ok: adds default arguments
默认参数与局部变量
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // calls screen(ht(), 80, ' ')
void f2()
{
def = '*'; // changes the value of a default argument
sz wd = 100; // hides the outer definition of wd but does not change the default
window = screen(); // calls screen(ht(), 80, '*')
}
- 局部变量wd hides the outer definition of wd,但这个局部变量与传给screen的默认参数无关
inline函数
含义
- A function specified as inline (usually) is expanded “ in line” at each call.
好处
- 函数调用的代价大:Registers are saved before the call and restored after the return; arguments may be copied; and the program branches to a new location
- inline函数可以避免上述代价
栗子
- shorterString是一个返回较短string的函数,若它是一个inline函数,则:
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
cout << shorterString(s1, s2) << endl;
// would be expanded during compilation into
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
注意
- inline specification is only a request to the compiler, the compiler may choose to ignore this request
- many compilers will not inline a recursive function
- inline函数可以多次定义(毕竟the compiler needs the definition, not just the declaration, in order to expand the code),但每次定义必须是一样的,因此为了避免麻烦,inline函数通常定义在头文件中
- functions defined in the class are implicitly inline
- 关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用
- 与宏定义的区别:宏定义是预编译时替换的,没有类型检查;内联函数是在编译时替换的,有类型检查
constexpr函数
定义
- a function that can be used in a constant expression
- return类型和参数类型必须是字面量
- 函数体 must contain exactly one return statement
性质
- constexpr函数是隐式inline的:the compiler will replace a call to a constexpr function with its resulting value
- 函数体也可包含其他语句,只要它们generate no actions at run time,比如null statements,type aliases和using声明
- constexpr函数的返回类型可以不是constant expression
// scale(arg) is a constant expression if arg is a constant expression
constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
int arr[scale(2)]; // ok: scale(2) is a constant expression
int i = 2; // i is not a constant expression
int a2[scale(i)]; // error: scale(i) is not a constant expression
- constexpr函数可以多次定义(毕竟the compiler needs the definition, not just the declaration, in order to expand the code),但每次定义必须是一样的,因此为了避免麻烦,constexpr函数通常定义在头文件中
debug帮手
assert
- 是一个preprocessor macro
- 用法:若expr==false,assert writes a message and terminates the program;若expr==true,assert does nothing
assert(expr);
- 定义在头文件cassert中
- preprocessor names are managed by the preprocessor not the compiler,所以可以直接使用preprocessor names,不需要用std::或using等
NDEBUG
- 是一个preprocessor variable
- If NDEBUG is defined, assert does nothing. By default, NDEBUG is not defined, so, by default, assert performs a run-time check.
- 定义NDEBUG的方法
// 法一
$ CC -D NDEBUG main.C
// 法二
#define NDEBUG
- 与_ _func_ _配合使用
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
cerr << _ _func_ _ << ": array size is " << size << endl;
#endif
// ...
}
PS: _ _func_ _可以打印正在debug的函数的名字;The compiler defines _ _func_ _ in every function. It is a local static array of const char that holds the name of the function.
- 与其他preprocessor names配合使用
if (word.size() < threshold)
cerr << "Error: " << _ _FILE_ _
<< " : in function " << _ _func_ _
<< " at line " << _ _LINE_ _ << endl
<< " Compiled on " << _ _DATE_ _
<< " at " << _ _TIME_ _ << endl
<< " Word read was \"" << word
<< "\": Length too short" << endl;
指向函数的指针
基本性质
- 函数的类型取决于返回类型和参数类型,比如 bool(const string&, const string&) 是一个函数类型
- 栗子
bool lengthCompare(const string &, const string &);
// pf points to a function returning bool that takes two const string references
bool (*pf)(const string &, const string &); // uninitialized
初始化
pf = lengthCompare; // pf now points to the function named lengthCompare
pf = &lengthCompare; // equivalent assignment: address-of operator is optional
PS:可以用nullptr或者zero-valued integer constant expression初始化函数指针,表示它不指向任何函数
使用指针调用函数
bool b1 = pf("hello", "goodbye"); // calls lengthCompare
bool b2 = (*pf)("hello", "goodbye"); // equivalent call
bool b3 = lengthCompare("hello", "goodbye"); // equivalent call
函数指针做函数的参数
- 和array一样,函数不能直接做函数的参数,但参数可以是指向函数的指针
// third parameter is a function type and is automatically treated as a pointer to function
void useBigger(const string &s1, const string &s2,
bool pf(const string &, const string &));
// equivalent declaration: explicitly define the parameter as a pointer to function
void useBigger(const string &s1, const string &s2,
bool (*pf)(const string &, const string &));
// automatically converts the function lengthCompare to a pointer to function
useBigger(s1, s2, lengthCompare);
函数指针做返回值
- 和array一样,函数不能直接做函数的返回值,但返回值可以是指向函数的指针
PF f1(int); // ok: PF is a pointer to function; f1 returns a pointer to function
F f1(int); // error: F is a function type; f1 can't return a function
F *f1(int); // ok: explicitly specify that the return type is a pointer to function
函数及函数指针的type aliases
// 以下两种定义等价,Func是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func;
// 以下两种定义等价,FuncP是函数指针类型
typedef bool(*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP;
using F = int(int*, int); // F is a function type, not a pointer
using PF = int(*)(int*, int); // PF is a pointer type
- 最好使用别名,不然定义会非常复杂
int (*f1(int))(int*, int);- 上面这个定义的含义:f1有参数list,所以f1是一个函数;f1前有个*,所以f1返回指针;The type of that pointer itself has a parameter list, so the pointer points to a function;That function returns an int.
- 等价于 auto f1(int) -> int (*)(int*, int);
关于临时变量
临时变量的特点
- invisible,在程序代码中没有显式出现
- non-named
产生临时变量的情形
- 值传递时
- 常引用传递且需要进行类型转换时
double d;
const int &ref=d;
// 其实相当于
int tmp=d;
const int & ref=tmp;
PS:c++不允许为非常引用产生临时变量,因为非常引用意味着可能会修改被引用的变量,若产生临时变量,则修改的是临时变量,没有意义。因此下面这段代码中的函数调用是错误的
void Fun(short &s) {
cout << "Fun" << endl;
}
int iTest = 20;
Fun(iTest); //error:iTest是一个int变量,但是函数参数为short型的引用,需要系统产生一个short型的临时变量,但这不被允许
- ++ --后置时:会生成一个临时对象tmp=原对象,再对原对象进行操作,但返回tmp
- 对象间的隐式类型转换
string str;
str=“abc”;
- 使用push_back/insert且参数是右值时
struct Bar {
Bar(int a) {}
explicit Bar(int a, double b) {}
};
int main(void)
{
vector<Bar> bv;
bv.push_back(1); // 隐式转换生成临时变量
bv.push_back(Bar(1)); // 显示构造临时变量
Bar b(1);
bv.push_back(b); // 没有临时变量
bv.emplace_back(1); // 没有临时变量
//bv.push_back({1, 2.0}); // 无法进行隐式转换
bv.push_back(Bar(1, 2.0)); // 显示构造临时变量
bv.emplace_back(1, 2.0); // 没有临时变量
return 0;
}
补充:c++编译过程
https://www.cnblogs.com/ericling/articles/11736681.html
补充:编译器相关
概述
- 编译器给对象分配内存的前提是知道其大小
- 声明是给编译器用的,定义是给连接器用的
函数的编译
- 代码中可以调用只声明未定义的函数,是因为在调用函数处,编译器会产生调用函数的代码,但编译器并不管函数的具体实现(即定义)在哪里,链接器才需要查找函数的实现代码并与函数调用代码对上;所以when we call a function, the compiler needs to see only a declaration for the function
- 类似地,when we use objects of class type, the class definition must be available, but the definitions of the member functions need not be present
- 所以we put class definitions and function declarations in header files and definitions of ordinary and class-member functions in source files(这样其他文件就可以只include .h文件)
模板的编译
- 与普通函数不同,编译器会生成实例化后的模板的代码,所以the compiler needs to have the code that defines a function template or class template member function
- 所以headers for templates typically include definitions as well as declarations
- when the compiler sees the definition of a template, it does not generate code(但会检查模板的语法等);编译器 generates code only when we instantiate a specific instance of the template
补充:常函数
- 只有成员函数才可以是常函数