4.1 表达式基础
表达式是由一个或多个运算对象组成,对表达式求值将得到一个结果
字面值和变量是最简单的表达式
可以通过一个或多个运算符把它们连接起来组成相对较复杂的表达式
4.1.1 基本概念
C++定义了一元运算符和二元运算符
运算符的优先级符合结合律
5+10*2+20/2
运算对象转换
整数会转换成浮点数,浮点数也可以转换成整数
只有相同类型才有运算的可能
重载运算符
本来 >> 和 << 是右移和左移运算符的,但在标准IO库把它重载了成了另外用途
cout << "I am SXTB" << endl;
左值和右值
表达式肯定是左值或右值中的一种
简单的区分方式,以 = 划分
左值 = 右值
左值可以幻化成右值,比如对对象取值就是充当右值过程,对对象赋值就是充当左值过程
但右值只能是右值,弟弟始终都是弟弟
使用关键字 decltype 的时候,左值和右值不同
int *p;
decltype(*p); // 因解引用会产生一个左值,所以结果是 int&
decltype (&P); // 因加上取地址符&,&p就是一个右值,所以结果是 int**
优先级和结合律
按照数学的思维即可,该加括号就加括号
cin >> v1 >> v2; // 先读入 v1,在读入 v2
int a = 0;
int b = 0;
cout << a+b << ++a << endl; // 别小看了,它是先计算 ++a, 再到 a+b 的,也就是从右往左的。
4.1.3 求值顺序
按照数学的思维即可,该加括号就加括号
4.2 算术运算符
+、 -、 *、 / 、%
布尔值不参与运算
bool b1 = true; // b1 是 true
bool b2 = -b; // b2 也是 true
运算符计算后可能会导致数据溢出,这是要注意的
char b = 0;
b += 255; // 发生溢出了,结果可能不是你想要的
4.3 逻辑和关系运算符
运算符 | 功能 |
! | 逻辑非 |
< | 小于 |
<= | 小于等于 |
> | 大于 |
>= | 大于等于 |
== | 相等 |
!= | 不相等 |
&& | 逻辑与 |
|| | 逻辑或 |
逻辑与和逻辑或运算符
A && B; // 只有当 A 和 B 都是真的时候,整个表达式才是真
A || B; // 在 A 和 B 中,只要其中有一个是真的,那整个表达式就是真的
// 在 A || B 中有一个地方需要注意的是,如果 A 为真了,那么程序就不会执行去判断 B 了
逻辑非运算符
bool b = true; // b 是 true
bool c = !b; // c 是 false
关系运算符
if( a < b < c) // 错误, 不能这么写
if(a < b && b < c) // 正确
4.4 赋值运算符 =
int a = 0;
int b = 0;
a + b = 20; // 这是不对的
20 = a; // 这是不对的
赋值运算满足右结合律
int a = 0;
int b = 0;
a = b = 10; // 正确
切勿混淆相等运算符和赋值运算符
if(a = b) // 条件恒为真
if(a == b) // 只有 a 和 b 相等,条件才为真
复合赋值运算符
a += 10; // 相当于 a = a + 10
b -= 10; // 相当于 b = b - 10
c *= 10; // 相当于 c = a * 10
d /= 10; // 相当于 d = d / 10
e %= 10; // 相当于 e = e % 10
// 还有 <<= >>= &= |= ^= 等等
4.5 递增和递减运算符
int a = 10;
int b = a++; // 先把 a 赋值给 b,最后 a 才自己加 1
int c = ++a; // a 先加1 ,然后再赋值给 c
a++ 为后置版本,++a 为前置版本,通过源码观看,貌似 ++a 会比 a++ 代码量少一下,应该会快一些,但现在硬件充足的时代,应该可以忽略不计,推荐还是使用前置版本
// 版本1
cout << *p << endl;
++p;
// 版本2,好像更简洁一些
cout << *p++ << endl;
4.6 成员访问运算符
点运算符的优先级比解引用的优先级高,所以需要括号括住
string s = "SXTB";
string *p = &s;
auto n = s.size();
n = (*p).size();
n = p->size(); // 等价于 (*p).size()
4.7 条件运算符
cond ? expr1 : expr2; // 当 cond 为真时整个表达式的结果就是 expr1 结果, 否则就是 expr2 结果
int a = 0;
int b = 1;
int c = 2;
a = (c > b ? c : b); // a 的结果是 2
可以嵌套
a = (a > b ? a : (b > c ? b : c));
4.8 位运算符
运算符 | 功能 |
~ | 取反 |
<< | 左移 |
>> | 右移 |
& | 与 |
^ | 异或 |
| | 或 |
4.9 sizeof() 运算符
获取数据类型占用的字节大小
int arr[5];
int *p;
char c;
sizeof(arr); // 5
sizeof(p); // 4
sizeof(c); // 1
4.10 逗号运算符
顺序按照从左往右方向
int a = 10, b = 10;
a = 20, b = 20;
4.11 类型转换
int a = 3.14 + 6; // 发生了自动转换,称为隐式转换
int a = (int)3.14 + 6; // 这个叫强制转换, 把3.14 强制转换了
4.11.2 其他隐式转换
int arr[10];
int *p = arr; // arr 自动转换成数组首元素的指针
4.11.3 显示转换
也就是强制转换,可能会丢失精度,转换时要做到心中有数
不然是非常危险的
命名的强制类型转换
reinterpret_cast
.
static_cast
只要不包含底层const,就能使用它进行转换
double val = static_cast<double>(j); // j 是 int 类型
const_cast
只能去掉底层const
const char *pc;
char *p = const_cast<char*>(pc); // 但通过 p 写值,是未定义行为
尽量避免强制类型转换,除非你清楚知道所发生的后果
5.1 简单语句
这些都是语句
a + 5;
cout << a;
空语句
;
千万别漏写分号,但也别多写分号
a += 10;; // 后面多一条空语句
while(1); // 这个分号是非常致命的!!导致了空循环
{
a += 1;
}
复合语句(块)
以花括号括住的
while(true){
a += 1;
b += 2;
}
5.2 语句作用域
以花括号括住的
5.3.1 if 语句
if(a > b)
{
///
}
else if(a > c)
{
///
}
else
{
}
5.3.2 switch 语句
记住不能少了 break,记住不能少了 break,记住不能少了 break
int a = 20;
switch(a)
{
case 10; cout << "S" << endl; break;
case 20; cout << "X" << endl; break;
case 30; cout << "T" << endl; break;
case 40; cout << "B" << endl; break;
default: break;
}
5.4.1 whilie 语句
while(condition)
statement
5.4.2 for 语句
for(init-statement; condition; expression)
statement
5.4.4 do while 语句
与 while 的区别就是,do while 先执行一遍在判定while 的条件
do
{
statement
}while(condition)
5.5 跳转语句
while(1)
{
if(a)
beak; // 结束 while 循环
}
while(1)
{
if(a)
conitnue; // 不往下执行了,也就不执行 a++ 了,继续从while开头开始
a++;
}
goto语句
goto end; // 直接跳转到 b++ 那里执行,跳过了 a++
a++;
end:
b++;
5.6 try 语句块和异常处理
异常是什么,比如
int a = 0;
int b = 29;
int c = b / a; // 分母不能为0哈,所以异常
这是比较简单,其实其他异常还是很难发现的
try{
program-statements
} catch(exception-declaration){
handler-statements
} catch(exception-declaration){
handler-statements
} //....
6.1 函数基础
函数是什么,一块代码块,代码块是什么,就是一条或多条语句组成的,函数名是什么,就是给这段代码块一个名字,如何调用,就是通过函数名执行(调用)代码块,函数调用的时候,有时候需要外部给传递一些数据,那些玩意儿叫参数,函数执行完了以后,你要告诉我吧,所以他就有返回值。
所以一个典型的函数:返回类型 + 函数名 + 0个或多个形参 + 函数体 组成
// int 是返回类型; max 是函数名;int a 和 int b 是形参
int max(int a, int b)
{
int c = a + b;
return c; // 返回值
}
int main(int argc, char* argv[])
{
cout << max(4, 5) << endl; // 函数调用;函数名+(),括号里面填参数
return 0;
}
函数可以重复调用
while(1)
{
max(4,5); // 循环调用
}
形参和实参
int max(int a, int b); // a 和 b 叫形参
max(4,5); // 函数调用的时候,4 和 5 叫实参
max(1,2); // 正确,形参和实参的数据类型相同
max(1,2,3); // 错误,形参就2个,实参却是3个
max("SXTB", 1); // 错误,形参和实参的数据类型不同
max(1); // 错误,形参就2个,实参却是1个
max(2.14,3.14); // 正确,实参隐式转换成int
形参列表可以为空,返回类型也可以为空
void fun1() { /*....*/}
void fun2(void) { /*....*/}
6.1.1 局部对象
就是函数体内定义的对象,它作用域就限在函数体内,叫局部变量吧,形参也是函数体内的一部分呢,所以当函数终止时,相对应的对象(非堆上申请)就会被销毁了
局部静态对象
void fun()
{
static int cnt = 0; // 就算函数终止了,它也还是存在的哦,除非整个程序都退出。
}
6.1.2 函数声明
void fun(); // 像这样的就是函数声明,没有函数体
函数声明一般放在头文件.h,函数定义就放在源文件.cpp
6.2 参数传递
有引用传递和值传递
值传递
#include <iostream>
#include <vector>
using namespace std;
void fun(int a, int b)
{
a = 3;
b = 4;
}
int main(int argc, char* argv[])
{
int x = 1;
int y = 2;
fun(x, y); // 此时,x 和 y 只是把它的值传递了进去,所以值传递
return 0;
}
引用传递
#include <iostream>
#include <vector>
using namespace std;
void fun(int& a, int& b)
{
a = 3;
b = 4;
}
int main(int argc, char* argv[])
{
int x = 1;
int y = 2;
fun(x, y); // 我们知道引用是别名,所以传递的变量本身,最后的结果是x和y的值变成了3和4
cout << x << "," << y << endl;
return 0;
}
指针传递
#include <iostream>
#include <vector>
using namespace std;
void fun(int* a, int* b)
{
*a = 3;
*b = 4;
}
int main(int argc, char* argv[])
{
int x = 1;
int y = 2;
fun(&x, &y); // 指针本身的值是对象的地址,所以间接的也是传递变量本身,最后的结果是x和y的值变成了3和4
cout << x << "," << y << endl;
return 0;
}
使用引用可避免拷贝
当一个对象特别大的时候,特别有用,如果不是引用传递采用值传递,拷贝是非常耗时的
如果不需要改变对象信息,一般建议使用 const 传递
bool fun(const string& str1, const string& str2)
{
return str1.size() < str2.size();
}
使用引用形参返回额外信息
我们知道函数只能返回一个值,然而有时候我需要返回多个值,这时候引用传递也可派上用场
int a = -1;
fun(a);
cout << a << endl; // 从 fun 中得到 a 的值
尽量使用常量引用
6.2.4 数组形参
如何传递数组
void print(const int *); // 数组可退化成首地址指针
void print(const int[]); // 就是传递数组
void print(const int[10]); // 10 表示期望有 10 个,但实际不一定有这么多
注意,传递的数组指针,函数内部只拿到数组指针却无法知道数组的大小的,不知道数组大小就不好操作遍历数组得到元素,可以使用一些额外信息得到数组大小
使用标记指定数组长度
就是自己定义一个结束标志,然后一直while
使用标准库
利用 begin 和 end 的方式来
显示传递数组大小
void print(int *p, size_t size); //其中 size 就是数组大小
6.2.5 main 处理命令行选项
int main(int argc, char* argv[])
{
/*
* argc: 表示 argv 数组中字符串的数量
* argv: 就是一个二维数组
* 如果 argc = 3,那么就有 argv[0] argv[1] argv[2] 三个
*/
return 0;
}
6.3 返回类型和 return 语句
int print()
{
int a = 0;
return a; // return 语句一定要放在函数的最后,不然 a+=1 将永远执行不了,除非有意为之
a += 1;
}
不要返回局部对象的引用和指针
作用域的时候说过,当函数终止时,局部对象(非堆非静态)将会被销毁,都销毁了,那你返回它有啥意义
引用返回左值
当然这只是演示,实际上我们不会这么用的
char& get_val(string& str, string::size_type index)
{
return str[index];
}
int main(int argc, char* argv[])
{
string s("SXTB");
get_val(s, 0) = 'A'; // 结果:SXTB -> AXTB
return 0;
}
递归
int fun(int a)
{
if (a > 1)
return fun(a - 1) + a; // 函数内又调用自己。。。俗称递归
return 1;
}
int main(int argc, char* argv[])
{
cout << fun(4) << endl; // 4 + 3 + 2 + 1 = 10
return 0;
}
递归函数最重要的一个点就截止条件
非必要不使用递归,非必要不使用递归,非必要不使用递归,除非你很清楚它什么时候结束
返回数组指针的函数
int arr[10]; // arr 是一个含有10个元素的数组
int *p1[10]; // p1 是一个含有10个指针的数组
int (*p2)[10]; // p2 是一个指针,它指向含有10个元素的数组
举个栗子
int (*func(int i))[10]; // 如何理解?
/* 进行拆分解析 */
func(int i) // 表示调用 func 函数时需要一个 int 类型的实参
(*func(int i)) // 表示可以对函数执行完毕后的结果进行解引用操作
(*func(int i))[10] // 表示解引用的结果是一个大小为 10 的数组
int (*func(int i))[10] // 表示数组内元素的数据类型是 int 类型
使用尾置返回类型
C++11 允许使用尾置返回
// 正常函数
int max(int a, int b);
// 尾置返回
auto max(int a, int b) -> int;
有啥好处?可以更直观一些
auto func(int i) -> int (*) [10];
还有可以利用 decltype
函数可以返回已知数组中某一个数组
int odd[] = {1, 3, 5, 7, 9};
int even[] = {0, 2, 4, 6, 8};
decltype(odd) *arr_ptr(int i) // decltype 得到的数组类型,不会自动转换成对应的指针,所以要加上 *
{
return (i % 2) ? &odd : &even;
}
6.4 函数重载
// 函数名字相同,形参列表不同就叫函数重载
// 以下通通都是函数重载
void max();
void max(int a);
void max(double a);
void max(int a, int b);
void max(int a, double b);
注意:main 函数是不可以重载的哦
那么程序是如何知道你调用的那一个重载版本的,根据你输入的实参类型自动推断出来的
void fun(int a);
void fun(double a);
fun(1); // 调用的是 void fun(int a) 版本
fun(3.14); // 调用的是 void fun(double a) 版本
注意顶层const 是无法区分重载的,比如,你不能这样
void fun(const int a); // 1
void fun(int a); // 不能这样,和 1 重复了
void fun(int *const p); // 2
void fun(int *p); // 不能这样,和 2 重复了
但是底层const 可以区分,以下都是正确的重载版本
void fun(int& a);
void fun(const int& a);
void fun(int *p);
void fun(const int *p);
在 C++ 语言中,编译器对函数名字查找会发生在类型检查之前
6.5.1 默认实参
// 函数有默认实参,也就是不用你输入就有一个默认值
void fun(int a, int b = 1);
fun(2); // 如果你不提供就使用了默认实参 a = 2, b = 1
fun(1,3); // 你提供了实参,所以默认值就不存在了, a=1,b=3
注意默认实参的位置,一般默认实参会放在相对位置的最右边
void fun(int a = 1, int b) ; // 错误,你不能这样
void fun(int a = 1, int b = 1); // 这样是可以的,都有一个默认实参
// 函数代用的时候,也不能省略
fun(,1) // 错误
fun(1) // 正确,你可以省略最尾部的实参
6.5.2 内联函数和 constexpr 函数
函数调用会有一系列操作,比如出栈入栈,这些都需要一下开销的,但,直接表达式求值就没有这样的开销,所以,内联函数就是把函数变成表达式,免去函数的开销,它是如果做的呢,它就是把函数体给成展开一句一句的表达式
有一个地方需要注意的时,你把函数声明为内联函数,编译器可能不会为你买单,也就是编译器不一定就把它搞成内联函数,看编译器的心情~~
还有一点,声明成内联函数的函数体不要太长,语句不要太多,一般几行以内吧,也不要大循环,最可怕的是递归~
constexpr 函数
前边笔记有记录 constexpr 用于常量表达式
所以定义 constexpr 函数有下面几个要求
1、返回值是字面值类型
2、形参是字面值类型
3、有且只有一条 return 语句
constexpr int count() { return 32; }
constexpr int fun = count();
constexpr int count() { return 32; }
int arr[count()]; // count() 是常量表达式
int i = 2;
int arr[i]; // 错误的哦,i不是常量表达式
6.5.3 调试帮助
assert 预处理宏
assert(expr); // expr 为假(0),则 assert 输出信息并终止程序
NDEBUG 预处理变量
__func__ // 输出当前调试的函数名字
__FILE__ // 存放文件名的字符串字面值
__LINE__ // 存放当前行号的整型字面值
__TIME__ // 存放文件编译时间的字符串字面值
__DATE__ // 存放文件编译日期的字符串字面值
6.6 函数匹配
重载的时候看匹配哪一个重载版本,最好是什么类型就传什么类型的实参
void fun(int a)
fun('a'); // 'a' 是 char类型, 会自动转变为 int
6.7 函数指针
函数指针指向的是函数哦,不是指向对象
*的优先级比较低,所以一般需要带上括号强化优先级
注意函数指针和指针函数
int* fun(); // 这不是函数指针哦,这是一个返回值为 int* 的指针函数
int (*fun)(); // 函数指针
使用函数指针
pf = fun; // fun() 是一个函数,fun函数名自动转换成指针赋值给 pf
pf = &fun; // & 是可选的
// 以下是调用过程
pf(); // 方式1
(*pf)(); // 方式2
重载函数的指针
符合重载函数匹配过程规则
void fun(int a);
void fun(doubel a);
void (*pf)(double) = fun; // 指定的是 void fun(doubel a) 版本
函数指针形参
int max(int a, intb);
typedef int (*funcp)(int a, int b); // 方式1
typedef decltype(max) *funcp; // 方式2 ,这看起立比较简单
// 函数指针形参
void fun(int a, funcp *p)
{
//....
p(1,4); // 调用
}
返回指向函数的指针
先看一下这个
using f = int(int*,int); // f 是函数类型,不是指针
using pf = int(*)(int*,int); // pf 是函数指针
然后就可以声明函数返回值
pf fun(int); // fun 是一个返回值为 pf 的函数
f* fun(int); // 等价于上边
看看下边
// 下面两条语句等价
int (*f1(int))(int*, int); // 看起来比较傻眼
auto f1(int) ->int(*)(int*,int); // 这样一下子就看出来了