C++学习笔记 二
以下内容为C++Primer三到六章的阅读笔记,包括 表达式 语句 函数
- C++学习笔记 二
- 表达式
- 求值顺序、优先级、结合律
- 强制类型转换
- 语句
- 异常处理
- throw表达式(用来“定义“异常)
- try catch
- 标准异常
- 异常处理
- 函数
- 管理数组实参的三种方法
- 数组引用形参(引用数组的形参)
- 不定长参数
- 返回值
- 递归
- 返回数组指针
- 函数重载
- 默认实参
- 内联函数
- constexpr函数
- NDEBUG宏定义与assert(expr)预处理宏
- 函数匹配
- 函数指针
- 作为形参的函数指针
- 利用typedef简化函数指针形参的声明过程
- 当然我们也可以将函数指针作为函数的返回值
- 最后最后还有auto decltype 可以使用
- 作为形参的函数指针
- 表达式
表达式
求值顺序、优先级、结合律
求值顺序即一个运算符(二元即以上)其参与运算的几个表达式的求值的顺序,如fun1()+fun2()
并未规定是先求fun1()
的值还是先求fun2()
的值,其跟优先级与结合律无关。
-
只有
&&
||
?:
,
四个运算符其表达式是规定了求值顺序的(从左向右求值),其余的运算符的求值顺序未知。因此在使用其他运算符的时候,若其中一个表达式改变了某个变量的值,则尽量不要在其他表达式中使用这个变量,例如以下这段代码while(p != s.end() && isspace(*p)) *p = toupper(*p++);
这段代码会给编译器造成困惑,因为
=
运算符的求值顺序是没看规定的,编译器可以先求右表达式的值,导致赋值给p
自增后的地址也可以先求左表达式的值。例如,
&&
运算符只有在左侧表达式为真的情况下才会求右侧表达式的值。index != s.size() && !isspace(s[index])
这样安排顺序可以保证右侧表达式的安全性 -
赋值运算符
=
的优先级较低,因此在条件语句中使用赋值运算符的时候要注意加上括号,并且这种写法的可读性更高例如:
//写法一 while(i != 10) i=getValue(); //写法二 √ while((i = getValue()) != 10);
-
++i
与i++
除非要使用变量修改后的值,否则尽量使用前置版本的自增(减),也节省性能消耗(对于复杂类型的迭代器)一个必须使用后置版本的例子
*p++
。
强制类型转换
强制类型转换的形式 cast-name
cast-name
的说明如下
-
static_cast
:具有明确定义的类型转换,如int -> double
char* -> string
例:
void *vp = &d; double *dp = static_cast
(vp); -
const_cast
:改变运算对象的底层const
,但不可以改变其类型:const char *pc; char *p = const_cast
(pc); -
reinterpret_cast
为运算对象的为模式提供较低层级上的重新解释(?)例如指针的转换int *ip; char *cp = reinterpret_cast
(ip); -
dynamic_cast
当然也可以使用旧式的强制类型转换
type(expr)
或(type) expr
语句
-
eles
的匹配问题:else
总与离他最近的 没有匹配的 那个if
相匹配。要注意块语句的存在问题。 -
switch case
语句中不允许跨过变量的初始化语句而跳转到其作用域的另一个位置。
异常处理
throw
表达式(用来“定义“异常)
当程序主动检测到异常的时候,抛出异常,后接异常类。
if(data < 0):
throw runtime_error("data must > 0");
try catch
当程序检测到已经”定义“好的异常后执行异常处理程序
int data = 10;
try
{
cout << "pls enter data" << endl;
cin >> data;
if (data < 10)
throw runtime_error("data must > 0");
cout << "no error";
}
catch (runtime_error err)
{
cout << err.what();
}
catch
的匹配过程:
当异常被抛出throw
后,程序会中断当前函数的执行过程(不会执行cout << "no error";
),在当前函数中开始匹配与异常相对应的catch
,若没有,则到 调用该函数的 函数中匹配catch
,以此类推。若最终没有与之相匹配的catch
,或抛出异常的时候根本没有try
,则程序会转到名为terminate
的标准库函数,该函数的行为与系统有关,一般情况下会导致程序的非正常退出。
标准异常
stdexcept
中定义的异常类
类名 | 解释 |
---|---|
exception |
最常见的问题(只允许默认初始化) |
runtime_error |
只有在运行时才能检测出的问题 |
range_error |
运行时错误:生成的结果超出了有意义的值域范围 |
overflow_error |
运行时错误:计算上溢 |
underflow_error |
运行时错误:计算下溢 |
logic_error |
程序逻辑错误 |
domain_error |
逻辑错误:参数对应的结果值不存在 |
invalid_argument |
逻辑错误:无效参数 |
length_error |
逻辑错误:试图创建一个超出该类型最大长度的对象 |
out_of_range |
逻辑错误:使用一个超出有效范围的值 |
bad_alloc
bad_cast
对象也只允许默认初始化,其余的异常对象在创建的时候都必须提供含有错误相关信息的初始值
函数
-
引用传递(passed by reference) 传引用调用(called by reference) :引用形参是其对应的实参的别名
-
值传递(passed by value)传值调用(called by value):形参和实参是两个相互独立的对象
-
使用引用而避免使用拷贝:拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本不支持拷贝操作。
- 当函数无需改变引用形参的值,最好将其申明为常量引用(对常量的引用)
- 通过使用引用形参(或指针)我们可以让函数返回多个值(作为引用形参传入)
-
当用实参初始化形参的时候会忽略形参的顶层
const
,类似于 我们可以用变量来初始化常量,但在函数中,我们还是只能读取const
形参的值而不能更改他;相同的,我们无法使用常量值(常量实参)来初始化非常量(变量)形参。
因此若我们把形参定义为常量引用可以大大扩大函数可接受实参的范围。若我们实在要在函数内使用普通引用,可以在函数内定义一个形参的副本。
管理数组实参的三种方法
当将数组作为实参传入函数的时候,函数会自动将其转换为指针
- 如果该数组本身就包含一个结束标记,那我们可以只传递一个指向该数组的头指针
例如C语言中的字符串,每个字符串都以‘\0’
结尾,因此我们可以在函数内判断指针是否移动到末尾。 - 传入头指针与尾指针
void print(const int *beg,const int *end);
- 传入头指针的同时传入数组长度
void print(const int ia[],size_t len);
这三种管理的本质就是为了防止操作指针的时候将指针移动到未定义的地址,而引发不可预知的错误。
并且以上关于引用的讨论同样也对指针适用
数组引用形参(引用数组的形参)
不同于将数组作为函数的形参函数,其会将其转换为指针并且无法携带有关数组长度的信息,但若将函数的形参定义为数组引用,则可以携带数组长度
void print(int (&arr)[10])
{
for(auto elem :arr)
cout << elem << endl;
}
不定长参数
-
相同类型的不定长参数可以使用模板类
initializer_list
类型的形参,其元素都为const
。
注意在传参的时候要将这些元素放在一个花括号里。void error_msg(initializer_list
errs) { for (auto err : errs) cout << err; } error_msg({"errorCode","errorMessage1","errorMessage2"}); -
省略符形参
参见C语言的
varargs
标准库void error_msg(...);
返回值
-
返回值为
void
类型的函数也可以使用return;
语句来提前结束函数的执行 -
注意不要返回局部对象的引用或指针,其内存将被释放,指向变量的引用或指针会失效。
-
有趣的是,若函数的返回值类型是非常量引用,则意味着该返回值是左值,那么该返回值就可以被放在赋值运算符的左边来给这个返回值赋值。
一般来说,左值意味着该对象(值)的内存空间,右值意味着该对象(值)的实际代表值;类似于指针与指针指向的对象
char& get_val(string& str, string::size_type ix) { return str[ix]; } int main() { string s("a value"); get_val(s, 0) = 'A'; cout << s << endl; } //输出为"A value"
-
返回值还可以是列表初始化 形如
{elem1,elem2,elem3}
,当然若返回值是内置类型,则列表中只能有一个值vector
func() { return{"elem1","elem2"}; }
递归
返回值中调用了自身的函数
返回数组指针
数组指针指针数组
int *p[10]; //指针数组,含有10个指针的数组(如同字符数组,数组在类型后面) int (*p)[10]; //数组指针,指向数组的指针,该数组有10个整数 type (*functionName(parameter_list) or valueName)[dimension];
- 因此我们应该这样写
int (*fun(const &int param1, const &int param2))[10];
- 我们也可以使用
typedef
来简化定义
typedef int newTypeName[dimension]; //newTypeName就是一个**含有dimension个整数的数组类形**
//这样以后数组指针只要加上*就可以了
newTypeName * fun(const &int param1, const &int param2);
- 还有一种方法便是使用尾置返回类型,通过
->
运算符将返回值类型至于函数后侧,前面使用auto
。
auto fun(const &int param1, const &int param2) -> int(*)[10];
- 当然,如果我们已经定义了与所要定义的函数返回值一样类型的变量,就可以用上
decltype
关键字了
int odd[] = {1,2,3};
int (*ap)[10];
decltype(odd) * fun(const &int param1, const &int param2);
decltype(ap) fun2();
函数重载
同一个作用域中,几个函数的名字虽然相同,但形参不同
-
不能依据是否拥有底层
const
来区别不同形参作为重载的依据,因为,常量和非常量都可用来初始化底层const
。 -
const_cast
在重载中的应用:有时候我们希望一个函数在传入常量的时候返回常量,而在传入非常量的时候可以放回非常量,这时候就可以用到
const
的强制类型转换。
注意 在这种情况下我们是明确强制类型转化后我们可以对该变量进行写操作,否则,强制将一个常数转化为非常数后,对其的操作往往是没有定义的。const string &shorterString(const string &s1,const string &s2)//针对常量的函数 { return s1.size() < s2.size() ? s1 : s2; } string &shorterString(string &s1, string &s2)//针对非常量的重载 { auto &temp = const_cast
( shorterString( const_cast (s1), const_cast (s2) ) ); return temp; } -
注意 不同或不同层级作用域中的函数都是相互独立的,无法跨越一个作用域来重载函数,并且,函数名的查找从内层开始,若在内存查找到某个要求的函数名,编译器就会忽略掉外层作用域中同名的函数(即使实参与形参不匹配,此时编译器会误以为只有内存作用域这些函数,而不会考虑外层作用与的同名重载)。
默认实参
- 形式
void fun(int param1 = 10);
- 局部变量不能用来初始化默认实参
- 通常,我们应该在函数申明中指定默认实参,并将申明放在适合的头文件中
内联函数
这种函数将在函数调用点内联的展开而无需跳转,用于流程简单直接,频繁调用的函数,关键词inline
inline const string &shorterString(const string &s1,const string &s2)//针对常量的函数
{
return s1.size() < s2.size() ? s1 : s2;
}
constexpr
函数
其隐式的被定义为inline
函数,这种函数类似于宏定义,在编译时就会将函数的返回值替换掉函数的调用表达式。
我们一般把这两者的定义放在头文件中
NDEBUG
宏定义与assert(expr)
预处理宏
略
函数匹配
略
函数指针
-
申明:
bool (*pfunc)(const string&,const string&);
(为初始化) -
使用
//可以使用与申明规格相同的函数来初始化这个函数指针 bool lengthCompare(const string& s1,const string& s2); pfunc = lengthCompare; //等价于 pfunc = &lengthCompare;
作为形参的函数指针
void func(bool pf(const string&,const string&));
void func(bool (*pf)(const string&,const string&));
利用typedef
简化函数指针形参的声明过程
-
定义函数类型 或 函数指针类型
类似于之前我们定义了 数组类型的 一种新类型typedef bool Func(const string&,const string&); //我们定义了一种叫 Func 的新类型,它是函数类型 typedef bool (*pFunc)(const string&,const string&); //我们定义了一种叫 pFunc 的新类型,他是一种 指针类型 并且是 指向函数的指针 //同样我们也可以使用 decltype 来简化这一过程 typedef decltype(lengthCompare) Func; //我们定义了一种叫 Func 的新类型,它是函数类型 typedef decltype(lengthCompare) * pFunc; //我们定义了一种叫 pFunc 的新类型,他是一种 指针类型 并且是 指向函数的指针 typedef decltype(pfunc) pFunc;//我们定义了一种叫 pFunc 的新类型,他是一种 指针类型 并且是 指向函数的指针 , 但这个语句其实没什么意义
-
像使用
int
bool
这类内建类型一样 使用我们的新类型吧!void aFuncWithFunctionParam(Func myFunctionParam1,pFunc FunctionParam2);//其实两个函数(指针)形参都是一样的。
当然我们也可以将函数指针作为函数的返回值
类似于将数组指针作为函数的返回值,我们有多种方法
但区别于函数指针形参的是,这次我们将区别函数指针与函数两个类型,我们只能使用函数指针类型来声明一个函数而不能用函数类型来声明一个函数
在这里顺便介绍一种定义类型别名的新方法using
using F = int(int*,int);
using pF = int(*)(int *,int);
F *aFuncReturnFuncPointor(int,int);
pF aFuncReturnFuncPointor2(int,int);
pFunc aFuncReturnFuncPointor3(int,int);
最后不要忘记了后置返回值 !
auto aFuncReturnFuncPointor4 -> int(*)(int,int); //√
auto * aFuncReturnFuncPointor4 -> int(int,int); //×
最后最后还有auto
decltype
可以使用
记住 decltype()
推到出来的结果是 非指针类型!