基础语法§
变量§
变量定义§
-
auto 5; auto 2.0f;
由编译器根据上下文确定数据类型。
-
int *ptr = new int; int *array = new int[10]; delete ptr; delete[] array;
指针变量的动态生成与删除。
new/delete是运算符,C++11包含的关键字,与malloc()/free()不同。
-
int v0; int &v1 = v0; // Right int &v2; // Wrong void swap(int &a, int &b) { a+=b; b=a-b; a-=b; }
左值引用。
具名变量的别名:类型名 &引用名 = 变量名。因为是已存在变量的别名,因此定义时必须初始化。
当函数参数为引用类型时,表示函数的形式参数与实际参数是同一个变量,改变形参将改变实参。
函数返回值可以是引用类型,但不可以是对局部变量的引用。
-
int && sum = 3+4; float && res = ReturnRvalue(f1, f2); void AcceptRvalueRef(T&& s){...}
右值引用。(C++11)
匿名变量(临时变量)的别名:类型名 && 引用名 = 表达式。
应用在函数参数中,能够减少临时变量拷贝的开销。
变量初始化§
初始化列表。
int a[] = {1, 3, 5};
int a[]{1, 3, 5};
int a(3+5);
int a{3+5};
int *i = new int(10);
变量类型推导§
使用decltype对变量或表达式结果的类型进行推导。
函数§
函数重载§
同一名称的函数,有两个以上不同的函数实现,被称为”函数重载“。要求参数不同。
函数参数的缺省值§
有缺省值的函数参数,必须是最后一个参数。
如果有多个带缺省值的函数参数,则这些函数参数只能在没有缺省值的参数后面出现。
void print(char* name,
int score,
char* msg = "pass")
{
cout<<name<<":"<<score<<","<<pass<<endl;
}
main函数§
关于main函数的命令行参数。
-
强制交互;
#include <iostream> int main() { int a, b; std::cin>>a>>b; std::cout<< a+b <<std::endl; return 0; }
-
参数传参;
#include <iostream> #include <cstdio> // atoi() int main(int argc, char** argv) { int a, b; a = atoi(argv[1]); b = atoi(argv[2]); std::cout << a+b << std::endl; return 0; }
char**
表示字符串的数组,也可用char*[]
。在命令
main2 4 5
执行后,argc
为3,argv[0]
为“main2”,argv[1]
为“4”,argv[2]
为”5“。 -
安全性改进。
#include <iostream> #include <cstdio> // atoi() int main(int argc, char** argv) { if (argc != 3) { std::cout << "Usage: " << argv[0] << " op1 op2" << std::endl; return 1; } int a, b; a = atoi(argv[1]); b = atoi(argv[2]); std::cout << a+b << std::endl; return 0; }
类§
类的定义§
- 定义类后,可以像语言内建的类型一样,用类来定义变量,该变量通常被称为”对象“。
- 通过”对象名.成员名“的形式,可以使用对象的数据或成员,或调用对象的成员函数,但仅限于访问public权限的成员。
- 在类外定义成员函数时,函数名前要加上类名限定,格式为:类名::函数名,其中,::称为”域运算符“。
- 所有成员函数的参数中,隐含着一个指向当前对象的指针变量,其名称为this。this指针是隐含的,但依旧是一个形参。
类成员的访问权限§
class Matrix{
public:
void fill(char dir);
private:
int data[6][6];
};
class Matrix{
int data[6][6]; // class成员的缺省属性为private
public:
void fill(char dir);
};
有时需要允许某些函数访问对象的私有成员,可以通过声明该函数为类的”友元“来实现。
#include <iostream>
using namespace std;
class Test{
int id;
public:
friend void print(Test obj);
...
};
void print(Test obj)
{
cout << obj.id << endl;
}
友元函数并非是类的成员函数,是一个类外的函数。
静态成员§
- 在类型前面加上static修饰的数据成员,是隶属于类的,称为类的静态数据成员,也称“类变量”;
- 静态数据成员被该类的所有对象共享(即所有对象中的这个数据域实际上处于同一内存位置)
- 静态数据要在实现文件中赋初值。
- 返回值类型前面加上static修饰的成员函数,称为静态成员函数,它们不能调用非静态成员函数(不具有隐含参数this);
- 类的静态成员既可以通过对象来访问,也可以通过类名来访问;
- 注意如果未定义拷贝构造函数等,类与对象静态成员的变化可能不一致。
常量成员§
- 使用const修饰的数据成员,称为类的常量数据成员,在对象的整个生命周期里不可更改;
- 常量数据成员只能在构造函数的初始化列表中设置,不允许在函数体中通过赋值来设置。
- 若用const修饰成员函数,则该成员函数在实现时不能修改类的数据成员,函数体中不允许有改变对象状态的语句。
- 若对象被定义为常量,则只能调用以const修饰的成员函数。
对象组合§
- 可以在类中使用其他类来定义数据成员,这种包含关系可嵌套;
- 子对象的初始化需要在初始化列表中完成,除非子对象能使用默认的构造函数;
- 对象的构造与析构次序:
- 先完成子对象构造,再完成当前对象的构造;
- 对象析构的次序与对象构造的次序相反。
构造函数析构函数§
构造函数§
- 类的构造函数由编译器自动生成调用语句,用于对象数据成员的初始化,以及其他初始化工作;
- 构造函数没有返回值类型,函数名与类名相同;
- 类的构造函数可以重载,即可以使用不同的函数参数进行对象初始化。
- 构造函数不应为私有类型,否则无法通过编译。
默认构造函数§
-
不带任何参数的构造函数,被称为”默认构造函数“,也称”缺省构造函数“;
-
使用默认构造函数(无参数)来生成对象时,对象定义的格式为:
ClassName obj;
不能使用ClassName obj();
。
构造函数的初始化列表§
-
位于参数列表后,以冒号作开头,使用“数据成员(初始值)”的形式。
class Student{ long ID; ... public: Student(long id) : ID(id) {} ... }
....
析构函数§
- 一个类只有一个析构函数,名称是“~类名”,没有函数返回值,没有函数参数;
- 编译器在对象生命期结束时自动调用类的析构函数,以便释放对象占用的资源,或其他后处理。
拷贝构造函数§
- 函数调用时以类的对象为形参或返回类对象时,编译器会生成自动调用“拷贝构造函数”,在已有对象基础上生成新对象;
- 是一种特殊的构造函数,是同类对象的常量引用;
- 当用同一个类的对象来作为参考定义一个新的对象时也调用拷贝构造函数,比如
A a; A b = a;
,等号处即调用拷贝构造函数,并非赋值。
#include <iostream>
using namespace std;
class Test {
public:
Test() { cout << "Test" << endl; }
Test(const Test& src) { cout << "Test(const Test&)" << endl; }
~Test() { cout << "~Test" << endl; }
}
void func1(Test obj) { cout << "func1()..." << endl; }
Test func2() {
cout << "func2()..." << endl;
return Test();
}
int main() {
cout << "main()..." << endl;
Test t;
func1(t);
t = func2();
return 0;
}
输出:
main()...
Test
Test(const Test&)
func1()...
~Test
func2()...
Test
~Test
~Test
移动构造函数(C++11)§
-
语法
ClassName(ClassName&&);
-
目的
- 用来利用“临时变量”中的资源(如内存);
- 临时变量被编译器设置为常量形式,使用拷贝构造函数无法利用此资源;
- 基于右值引用定义的移动构造函数支持接受临时变量,甚至得到临时变量中的资源。
Test(Test&& t) : buf(t.buf) { cout << "Test(Test&&) called. this->buf @ " << hex << buf << endl; t.buf = nullptr; }
-
注意:编译器不要对返回值优化。
-fno-elide-constructors
编译器生成的函数成员§
-
默认构造函数 -- 空函数;
-
析构函数 -- 空函数;
-
拷贝构造函数 -- 按位复制对象所占内存,对指针不正确;
-
移动构造函数 -- 与默认拷贝构造函数相同;
-
赋值运算符重载 -- 与默认拷贝构造函数相同;
-
如果用户定义,编译器即不再生成;
-
显示缺省:
T() = default; ... T::T() = default;
重载§
赋值运算符重载§
- 赋值运算符函数是在类中定义的特殊成员函数。
- 拷贝构造函数与赋值运算符重载函数的区别,一用于初始化,另用于赋值。
ClassName& operator= (const ClassName& right)
{
if(this != &right) { // 避免赋值给自己
// 将right对象中的内容复制到当前对象中
}
return *this; // 注意返回内容
}
流运算符重载§
istream& operator>> (istream& in, Test& dst);
ostream& operator<< (ostream& out, const Test& src);
可以将流运算符函数声明为类的友元,可访问对象的私有成员。
函数运算符重载§
函数运算符()也能重载,它使对象看上去像是一个函数名。重载的对象可完成一些操作,看上去像一个函数,故也称“函数对象”。有状态有记忆的操作。
ReturnType operator() (Parameters) {
...
}
ClassName Obj;
Obj(real_parameters);
// -> Obj.operator() (real_parameters);
数组下标运算符重载§
-
函数声明形式:
返回类型 operator[] (参数)
-
如果返回类型是引用,数组运算符调用可以作左值,否则只能作右值。
Obj[index] = value; // 返回值为引用 Var = Obj[index];
重载前缀和后缀运算符§
-
前缀运算符重载声明
ReturnType operator++(); ReturnType operator--();
-
后缀运算符重载
ReturnType operator++(int dummy); ReturnType operator--(int dummy);
通过在函数参数中的哑元参数dummy来区分前缀与后缀的同名重载。哑元:在函数体语句中没有使用该参数。
智能指针(C++11)§
对内存回收提供一定支持。
-
unique_ptr
不允许多个指针共享资源,可以用标准库中的move函数转移指针
-
shared_ptr
多个指针共享资源
-
weak_ptr
可复制shared_ptr,但其构造或释放对资源不产生影响
面向对象§
继承§
-
在已有类的基础上,可以通过继承来定义新的类,实现对已有代码的复用;
-
继承方式,
class Derived : [private] Base {...} class Derived : public Base {...}
缺省继承方式为private继承。
-
被继承的已有类,被称为基类,也称父类;
-
通过继承得到的新类,被称为派生类,也称子类、扩展类。
-
基类中的数据成员通过继承称为派生类对象的一部分,需要在构造派生类对象的过程中调用基类构造函数来正确初始化。
- 若没有显式调用,编译器自动生成一个对基类的默认构造函数的调用;
- 若要显式调用,则只能在派生类构造函数的初始化列表中进行。
-
先执行基类构造函数初始化继承到的数据,再执行派生类构造函数。
-
对象析构时,先执行派生类析构函数,再执行由编译器自动调用的基类的析构函数。
继承基类构造函数§
- 在派生类中使用
using Base::Base()
来继承基类的构造函数,相当于给派生类定义了相应参数的构造函数; - 如果基类的某个构造函数被声明为私有成员函数,则不能在派生类中声明继承该构造函数;
- 如果派生类使用了继承基类构造函数,编译器就不会再为派生类生成默认构造函数。
派生类中的私有成员§
- 基类中的私有成员,不允许在派生类对象和成员函数中被访问;
- 基类中的公有成员:
- 若是使用public继承方式,则称为派生类的公有成员,基类接口成为子类接口的一部分,子类对象可调用
- 若是使用private继承方式,则只能供派生类成员函数访问,不能被派生类对象访问,基类接口不许子类对象调用
重写§
派生类重写基类成员函数§
- 基类中已定义的成员函数,可在派生类中重新定义,称为“函数重写”(override);
- 重写发生时,基类中该成员函数的其他重载函数都将被屏蔽掉,不能提供给派生类对象使用;
- 可以在派生类中通过using恢复指定的基类成员函数,即去屏蔽,重写的函数具有更高优先级。
虚函数§
-
对于被派生类重写的成员函数,若它在基类中被定义为虚函数,则通过基类指针或引用调用该成员函数时,编译器将根据对象实际类型决定是调用基类中的函数还是派生类重写的函数;
-
只有重写函数定义为虚函数,当派生类对象转换为基类对象作用时,可以根据对象实际类型调用对应重写的函数,实现多态的效果;
-
若某成员函数在基类中声明为虚函数,当派生类重写它时,无论是否声明为虚函数,该成员函数都仍然是虚函数。
-
虚析构函数使得派生对象析构时会先调用派生类的析构函数,再调用基类的析构函数,而若析构函数非虚函数,则代码运行只会按照要求参数的类型执行调用,当派生对象类型转换为基类对象,调用析构函数时会只调用基类析构函数。
一句话:指向派生类的基类指针若调用虚函数,则实际执行的是派生类的函数定义。
-
禁止重写的虚函数(C++11),使用final关键字修饰的虚函数,派生类不可对它进行重写;
-
final可以在继承关系链的“中途”进行设定,禁止后续派生类对指定虚函数重写;
-
纯虚函数,将函数的实现推迟到子类
public: virtual void fun() = 0;
在虚函数声明最后加上“=0”,使得类称为抽象类,包含纯虚函数的类不可以定义对象,所以只能做基类,这种抽象类也称为接口类,只用来规定接口。
类型转换§
对象类型转换§
- 派生类对象转换成基类对象,称为向上映射。而基类对象转换成派生类对象,则称为向下映射;
- 向上映射可以由编译器自动完成,是一种隐式的自动类型转换;
- 凡是接受基类对象的地方,都可以使用派生类对象,编译器会自动将派生类对象转换为基类对象以便使用。
自定义类型转换§
-
在源类中定义“目标类型转换运算符“;
class Src { ... operator Dst() { return Dst(); } ... }
-
在目标类中定义”源类对象作参数的构造函数“,本身有作为构造函数直接构造的作用,也能够完成自动类型转换的工作;
class Dst { ... Dst(const &Src s) {} ... }
-
禁用自动类型转换,使用
explicit
关键字放于上述两种方法定义的前一行;也可以在函数定义末尾加上= delete
表示不能调用。
强制类型转换§
自动类型转换为隐式转换,强制类型转换被称为显式转换。
- dynamic_cast<Dst_Type>(Src_var)
- Src_var必须是引用或指针类型,Dst_Type类中含有虚函数,否则会有编译错误
- 若目标类与源类之间没有继承关系,则无法转换,返回空指针
- static_cast<Dst_Type>(Src_var)
- 基类对象不能转换成派生类对象,但基类指针可以转换成派生类指针
- 派生类对象(指针)可以转换为基类对象(指针)
- 没有继承关系的类之间,必须有转换途径才可以,自定义或语言支持
模板§
template <typename T>
类模板 -实例化-> 类 -实例化-> 对象
类模板的成员函数,也可有额外的模板参数。
模板参数的特化§
有时,有些类型并不适用,则需要对模板进行特殊化处理。
对函数模板,如果有多个模板参数,则特化时必须提供所有参数的特例类型,不能部分特化。
template<typename T>
T sum(T a, T b)
{
return a + b;
}
template<>
char* sum(char* a, char* b)
{
char* p = new char[strlen(a) + strlen(b) + 1];
strcpy(p, a);
strcat(p, b);
return p;
}
对于类模板。
template <typename T>
class Sum {
T a, b;
public:
Sum(T op1, T op2) : a(op1), b(op2) {}
T DoIt() { return a + b ;}
};
template <>
class Sum<char*> {
char *str1, *str2;
public:
Sum(char* s1, char* s2) : str1(s1), str2(s2) {}
char* DoIt() {
char* tmp = new char[strlen(str1) + strlen(str2) + 1];
strcpy(tmp, str1);
strcat(tmp, str2);
return tmp;
}
};
UML类图§
类名:Calculator
实现:-_applePrice : float
-_bananaPrice : float
接口:+calTotal(appleWeight : float, bananaWeight : float) : float
....
面向对象编程§
针对接口而不是针对实现编程§
- 通过抽象出“抽象概念”,设计出描述这个抽象概念的抽象类,或称接口类,这个类有一系列的纯虚函数,描述了这个类的接口;
- 对这个接口类进行继承并实现这些纯虚函数,从而形成这个抽象概念的实现类——实现可以有很多种以适应变化;
- 在使用这个概念时,我们使用接口类来引用这个概念,而不直接使用实现类,从而避免实现类的改变造成整个程序的大规模修改。
单一责任原则§
- 类的功能应该是内聚的,一个类只承担一项功能;
- 表现为:修改/派生一个类只应该有一个理由,只能够由单个变化因素引起。
容器、迭代与算法§
- 容器:存储数据,数据的表示
- 算法:处理数据,抽象的算法实现
- 迭代器:标准的数据遍历接口,隔离算法与容器,使算法与数据的表示无关
运算符重载就是在新的数据类型上还原运算符的本质。
内联函数§
- inline
- 作用:函数内联展开,避免函数调用开销,用空间换时间
- 在类定义体内定义(实现)的函数缺省为内联函数
泛型编程§
- 先实现算法,再充实数据表示(类型)
- 实现算法/抽象结构与数据表示之间的分离
”变“与”不变“§
- 算法/抽象结构的数据接口与实现均不变
- 数据的访问接口不变(迭代器)
- 数据的可用操作不变(操作符重载)
- 数据的组织形式可变
- 数据的类型(值域、存储、操作实现)可变
STL标准模板库§
- 一组最常用的C++功能的模板实现
- 算法:即常用算法功能的模板实现(min、max、for_each、find_if、copy、sort、stable_sort等)
- 函数对象及其操作:(greater、less、equal_to、logical_and、logical_or、not1、not2、ptr_fun等)
- 容器及其迭代器:算法所作用的一组数据及对其进行遍历的手段(vector、dqueue、list、set、map、stack、queue及其迭代器,istream_iterator和ostream_iterator等)
- 其他
使用继承实现接口类,重写所有虚函数。
设计模式§
策略模式§
如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
Bridge模式§
把抽象部分与实现部分分离,使它们都可以独立变化。
适配器模式§
功能不变,接口变化。
使用组合实现适配,称作对象Adapter。
代理模式§
接口不变,功能变化。
用于对被代理对象进行控制,如引用计数控制、权限控制、延迟初始化等。
装饰器模式§
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
对比策略模式:
- 相同点
- 通过对象的组合修改对象的功能
- 以组合替代继承,更加灵活
- 不同点
- 策略模式需要修改对象功能内部
- 装饰模式修改对象功能的外部
责任链模式§
有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
参考资料§
[1] 面向对象程序设计(C++)(2019春)- 学堂在线 - 徐明星
[2] 设计模式 | 菜鸟教程