课程网址:link - 北京邮电大学 - 崔毅东
C++概览
C++编程范式
- Structural Programming (结构化编程)
- Object-Oriented Programming (OOP,面向对象编程)
- Generic Programming (GP,泛型编程)
- Functional Programming (FP,函数式编程)
高级语言的演化
C语法的增强及对应的C++11特性
引用(Reference)
引用类型做函数的参数:引用传递
You can use a reference variable as a parameter in a function and pass a regular variable to invoke the function. (引用可做函数参数,但调用时只需传普通变量即可)
When you change the value through the reference variable, the original value is actually changed. (在被调函数中改变引用变量的值,则改变的是实参的值)
下面的案例分别用普通变量、指针和引用三种形式来调用实参:
可以看到第一种,在调用函数时实参的值传递给形参,但实参在调用后不发生改变。
而第二种和第三种,在调用函数时实则对实参所在内存地址中存的值进行了改变。
(引用可以简单理解为const指针,一旦初始化就无法再绑定到其他变量上)
空指针(nullptr)
0带来的二义性问题
- C++03中,空指针使用“0”来表示。0既是一个常量整数,也是一个常量空指针。
- C语言中,空指针使用(void *)0来表示。
- 有时候,用“NULL”来表示空指针(一种可能的实现方式是#define NULL 0)。
C++标准化委员会希望“空指针”是一个确定的东西。
C++11中引入保留字“nullptr”作为空指针。
动态内存管理:分配/释放(Dynamic memory management: Allocate/Release)
C++中通过运算符new申请动态内存
new <类型名> (初值) ; //申请一个变量的空间
new <类型名>[常量表达式] ; //申请数组
如果申请成功,返回指定类型内存的地址;
如果申请失败,抛出异常,或者返回空指针(nullptr)。(C++11)
动态内存使用完毕后,要用delete运算符来释放
delete <指针名>; //删除一个变量/对象
delete [] <指针名>; //删除数组空间
布尔数据类型(Boolean data type)
直接输出布尔类型变量会输出结果0或1。
用std::boolalpha输出布尔类型变量会输出结果false或true(用字符形式输出)。
列表初始化(List Initialization)
C++11标准之前的初始化方法
int x = 0;
int y(2);
char c('a');
int arr[] = { 1,2,3 };
char s[] = "Hello";
C++11标准仍然支持旧的初始化方法。
列表初始化
列表初始化是C++11的一个新特性,“列表”是用花括号括起来的一些值。
列表初始化的两个分类
- Direct list initialization (直接列表初始化)
- Copy list initialization (拷贝列表初始化)
//直接列表初始化
/* Variable initialization */
int x{}; // x is 0;
int y{ 1 }; // y is 1;
/* Array initialization */
int array1[]{ 1,2,3 };
char s1[ 3 ] { 'o', 'k' };
char s3[]{ "Hello" };
//拷贝列表初始化
/* Variable initialization */
int z = { 2 };
/* Array initialization */
int array2[] = { 4,5,6 };
char s2[] = { 'y','e','s' };
char s4[] = { "World" };
char s5[] = "Aloha"; // Omit curly braces (省略花括号)
在能使用列表初始化的地方优先使用列表初始化,因为当发生隐式类型转换丢失精度的时候(窄化narrowing),列表初始化会编译出错进行提示。
如:int x { 2.5 };
类型转换(Type conversion)
C风格强制类型转换
语法:(type) value
printf("%d", (int) 2.5);
C++风格强制类型转换
语法:static_cast< type > value
cout << static_cast< double >(1) / 2;
C++11 对类型系统的增强(C++11 Enhancement for Type System)
类型(Type)是贯穿于计算机程序中的概念
-
数据类型 (Data type)
int, long int, double,
struct, char *, float [], int (*f)()… -
计算机程序构造块(Constructs of a Computer Program)
计算机程序构造块是不同大小粒度的计算机程序组成部分,它包括变量、表达式、函数或者模块等。
什么是类型系统(What is Type System)
在编程语言中,“类型系统”是将“type”属性指定给不同计算机程序构造块的规则集。
这些类型规范并强制程序员用于数据结构和组件的其它隐式类别(e.g. “string”, “array of float”, “function returning boolean”)。
为什么使用类型系统(Why using Type System)
类型系统可以减少程序中可能出现的bug。
类型系统减少BUG的方法是:
- 定义不同程序块间的接口
- 检查多个块之间是否以一致的方式连接在一起
静态类型 v.s. 动态类型
程序设计语言的类型系统机制会检查连接在一起的多个块的一致性。
上述检查若发生在编译期,称为静态类型 (如C、C++、Java等编译语言)。
上述检查若发生在运行时,称为动态类型 (如Python等脚本语言)。
上述检查若同时存在于编译期和运行时,称为混合类型。
自动类型推导:auto关键字(Automatic Type Deduction: auto)
关键字 auto
C++03及之前的标准中,auto放在变量声明之前,声明变量的存储策略。但是这个关键字常省略不写。
C++11中,auto关键字放在变量之前,作用是在声明变量的时候根据变量初始值的类型自动为此变量选择匹配的类型。
auto的使用限制
- auto 变量必须在定义时初始化,这类似于const关键字
auto a1 = 10; //正确
auto b1; //错误,编译器无法推导b1的类型
b1 = 10;
- 定义在一个auto序列的变量必须始终推导成同一类型
auto a4 = 10, a5{20}; //正确
auto b4{10}, b5 = 20.0; //错误,没有推导为同一类型
- 如果初始化表达式是引用或const,则去除引用或const语义。
int a{10}; int &b = a;
auto c = b; //c的类型为int而非int&(去除引用)
const int a1{10};
auto b1 = a1; //b1的类型为int而非const int(去除const)
- 如果auto关键字带上&号,则不去除引用或const语意
int a = 10; int& b = a;
auto& d = b;//此时d的类型才为int&
const int a2 = 10;
auto& b2 = a2;//因为auto带上&,故不去除const,b2类型为const in
- 初始化表达式为数组时,auto关键字推导类型为指针。
int a3[3] = { 1, 2, 3 };
auto b3 = a3;
cout << typeid(b3).name() << endl; //输出int * (输出与编译器有关)
- 若表达式为数组且auto带上&,则推导类型为数组类型。
int a7[3] = { 1, 2, 3 };
auto& b7 = a7;
cout << typeid(b7).name() << endl; //输出int [3] (输出与编译器有关)
- C++14中,auto可以作为函数的返回值类型和参数类型
typeid()运算符可以输出类型信息,typeid().name()可以输出类型名。(需要加入< typeinfo >头文件才可以使用)
注意使用typeid()能显示出指针类型但并不能显示出引用类型的变量,需要通过断点调试去监视面板观察。
另外,auto不能用于声明数组,否则无法通过编译,报告错误:auto类型不能出现在顶级数组类型中。
数组的初始化是在运行期间完成的,而auto是在编辑器间完成的。当 auto x[] = {1, 2, 3}; 时,auto无法推导出数组元素的类型。
在能用auto的时候尽量用,保证代码的正确性、性能、可维护性、健壮性,以及方便。
自动类型推导:decltype(Automatic Type Deduction: decltype)
decltype = Declare Type(利用已知类型声明新变量)
decltype 主要用于泛型编程(模板)
关键字decltype的用法
decltype利用已知类型声明新变量。
有了auto,为什么还要整出一个decltype?原因是,我们有时候想要从表达式的类型推断出要定义的变量类型,但不想用该表达式的值初始化变量。
decltype是在编译期推导一个表达式的类型,它只做静态分析,因此它不会导致已知类型表达式执行。
代码示例
#include<iostream>
using namespace std;
int fun1() { return 10; }
auto fun2() { return 'g'; } // C++14
int main(){
// Data type of x is same as return type of fun1()
// and type of y is same as return type of fun2()
decltype(fun1()) x; // 不会执行fun1()函数
decltype(fun2()) y = fun2();
cout << typeid(x).name() << endl;
cout << typeid(y).name() << endl;
return 0;
}
decltype与auto的对比
decltype和auto都是C++11自动类型推导的关键字。它们有很多差别:
auto忽略最上层的const,decltype则保留最上层的const。
auto忽略原有类型的引用,decltype则保留原有类型的引用。
对解引用操作,auto推断出原有类型,decltype推断出引用。
auto推断时会实际执行,decltype不会执行,只做分析。
总之在使用中过程中和const、引用和指针结合时需要特别小心。
简化的C++内存模型(Simplified Memory Model)
常量和指针(Constant and Pointer)
常量和指针搅合在一起,成为一个比较麻烦的问题。就像你想分清楚“己巳已”不容易一样,想搞清楚“指针常量”、“常量指针”、“常量指针常量”这种拗口的东西,总得花点心思。
问题还不是记性好不好,因为指针和常量搅合在一起,还涉及到函数传参、C风格数组、字符串常量这些应用场景,就更得仔细一些。
指针
指针是一个地址,它长得像 0x8FFF 这个样子。地址呢,就是某个内存位置的一个编号。那这个位置的内存是可以存放一些数据的。这些数据就叫做“指针所指的数据”或者“指针指向的数据”。
常量和指针
我们把指针放到一个变量里面,就是指针变量;
我们把指针放到常量中,就是指针常量;
那如果一个指针(也就是地址,比如0x8FFF)所指的数据(也就是0x8FFF这个内存位置存放的数据)是常量,这个指针被称为常量指针。
所以,有一种东西,叫做“常量指针常量”。就是说,一个常量中存着一个指针,这个指针又指向另外一个常量。
Pointer to Constant (常量指针/常指针)
特征:指针所指向的内容不可以通过指针的间接引用(*p)来改变。
const int x = 1;
const int* p1;
p1 = &x; //指针 p1的类型是 (const int*)
*p1 = 10; // Error!
char* s1 = "Hello"; // Error!
const char* s2 = "Hello"; // Correct!
Pointer Constant (指针常量)
“指针常量”的含义是:指针本身的内容是个常量,不可以改变。
int x = 1, y = 1;
int* const p2 = &x; //常量 p2的类型是 (int*)
*p2 = 10; // Okay! à x=10
p2 = &y; // Error! p2 is a constant
注意:数组名就是数组的首地址的别名。现在可以说:数组名就是一个指针常量。
int a[] = { 1, 2, 3 };
a = a+1; //Error
指针和常量的总结
const int * x
int * const y
在前先读,在前不变
指针和常量谁在前先读谁 ;* 代表被指的数据,名字代表指针地址。
const在谁前面谁就不允许改变。
using, typedef, and #define的用法(Usage of using, typedef, and #define)
using 声明新的类型名称
当我们声明这样一些变量时:
const unsigned long int * p;
const unsigned long int * q;
const unsigned long int * r;
我们会觉得很麻烦。
那有没有一种办法使得 p, q, r的类型声明简便一点呢?
我们在C语言里面学了 typedef,它可以声明一个新的类型名。
typedef const unsigned long int * MyPointer;
MyPointer p;
C++11中为 using 关键字赋予了一个类型声明的新功能:
using ConstPointer = const unsigned long int *;
ConstPointer p;
ConstPointer q;
ConstPointer r;
using的写法比typedef的写法更加直观,所以,我们应尽量使用using声明新类型名。而且当涉及到模版类型名时,只能使用using。
编码规范
Names representing types must be in mixed case starting with upper case.
代表类型的名字必须首字母大写并且其它字母大小写混合
例如:Line, SavingsAccount
变量的作用域
变量的作用域分类
- 全局作用域:全局变量
- 局部作用域:局部变量
(局部作用域可以分为:文件作用域、函数作用域以及函数内部的块作用域。)
如果外部代码块与其内嵌代码块有同名的变量,那么会产生同名覆盖这种现象。此时要遵循“就近原则”来判断哪个同名变量起作用
一元作用域解析运算符(Unary Scope Resolution)
If a local variable name is the same as a global variable name, you can access the global variable using ::globalVariable. (局部变量名与全局变量名相同时,可使用 :: 访问全局变量)
The :: operator is known as the unary scope resolution.(:: 这个运算符被称为一元作用域解析运算符)
#include <iostream>
int v1 = 10;
int main() {
int v1 = 5;
std::cout << "local variable v1 is " << v1 << std::endl;
std::cout << "global variable v1 is " << ::v1 << std::endl;
return 0;
}
重载函数
重载函数是在同一个名字空间中存在两个或者多个具有相同名字的函数所构成的语法现象。
调用重载函数的语句,是由编译器在编译期确定的。
编译器判断某个函数调用语句所对应的重载函数时,判断依据是函数参数的类型、个数和次序。
如果编译器无法判定,就会报告二义性错误。
带有默认参数值的函数
函数的参数可以指定默认值。
指定默认值时,要保证带有默认值的参数要位于函数参数列表的右侧。
调用带有默认参数值的函数时,如果不指定带有默认值的参数,则该参数自动被赋为默认值
C++规定(C++03/C++11): A default argument shall not be redefined by a later declaration (not even to the same value). (函数重定义/声明时,不允许重定义默认参数)
int Add (int a, int b = 3); // 原型声明
int Add (int a, int b = 3) { // 错误!不能重定义默认参数值,尽管与原型声明相同 }
内联函数(Inline Function)
普通函数的优缺点
-
Pros(优点): 易读易维护
-
Cons (缺点): 调用时有开销
函数调用时:参数及部分CPU寄存器的内容进栈,控制流跳转。
函数返回时:返回值及寄存器值出栈,控制流跳转。
使用内联函数
目的:减小函数调用开销
方法:代码插入到调用处
结果:导致程序变大
定义内联函数
定义函数时,在函数类型前面加上 inline 关键字,则该函数就成为内联函数。
一般而言,内联函数的声明和定义都在一起。我们很少将内联函数的声明和定义分开编写。
// 定义内联函数
inline int max (int a, int b) {
return (a > b ? a : b);
}
// Calling (调用内联函数)
int x = max (3, 5);
int y = max (0, 8);
// Inline expansion(内联展开)
int x = (3 > 5 ? 3 : 5);
int y = (0 > 8 ? 0 : 8);
内联函数的使用
编译器在遇到内联函数的调用时,会将内联函数的函数体展开到调用位置,从而避免函数调用的开销。
一般来说,内联函数只有在需要考虑程序运行性能的环境中才使用。
程序员所用的 inline 关键字,只是对编译器的一个请求。内联函数是否展开,是由编译器决定的。
将内联函数的声明和定义分离
在C++标准7.1.2.4节有如下说明
An inline function shall be defined in every translation unit in which it is odr-used and shall have exactly the same definition in every case (3.2). [ Note: A call to the inline function may be encountered before its definition appears in the translation unit. —end note ]
内联函数应在每个翻译单元中定义。在该翻译单元中它遵循“单一定义规则(ODR)”,并且所有该内联函数定义必须完全相同。[注释:在翻译单元中可能会在内联函数定义出现之前就有调用该内联函数的语句]
因此,内联函数声明和定义分类的用法如下:
#include <iostream>
inline void foo();
int main() {
foo();
}
inline void foo() {
std::cout << "Hi\n";
}
Range-based for-loop (基于范围的for循环)
基于范围的for循环的语法
for( 元素名变量 : 广义集合) { 循环体 }
- “元素名变量”可以是引用类型,以便直接修改集合元素的值。
- “元素名变量”也可以是const类型,避免循环体修改元素的值。
- 其中“广义集合”就是“Range(范围)”,是一些元素组成的一个整体。
“广义集合”例子
auto a1[] { 1, 3, 5, 7 };
std::array<int, 4> a2 { 2, 4, 6, 8};
std::vector< int > v = { 42, 7, 5 };
std::vector< std::string > s { “Hello”, “World”, “!”};
用法
想要操作某个广义集合中的所有元素,那么只需要关心:
- 从集合中取出某个元素
- 保证所有元素都被遍历
例:把数组a的元素都输出到屏幕上,然后把数组的元素翻倍。
int a[] = { 2,1,4,3,5 };
for (auto i : a) {
std::cout << i << std::endl;
}
for (auto& i : a) {
i = 2 * i;
}
限制
基于范围的循环仅限于for语句。
do…while(); 和while(){} 不支持基于范围的循环。