- {}列表初始化
花括号里没有提供初始值,对于基本类型的变量,初始值将默认为0。
int a{};
int b{2};
-
auto
定义有初始化的变量时,可以用auto推断其数据类型。#include <iostream> using std::cout; using std::endl; int main() { auto b = true; auto ch{ 'X' }; auto i = 123; auto d{ 1.2 }; auto z = d + i; cout << typeid(b).name() << endl; // bool cout << typeid(ch).name() << endl; // char cout << typeid(i).name() << endl; // int cout << typeid(d).name() << endl; // double cout << typeid(z).name() << endl; // double return 0; }
-
decltype
用decltype(exp)得到一个表达式的值的类型,并用这个类型来定义一个变量。decltype(3 + 4.5) c; cout << typeid(c).name() << endl;
-
const
用const关键字修饰一个变量(对象),用来表示变量的不可修改性。
因为const定义的变量i是不能被修改的,因此在定义这个变量时就必须初始化,如果不初始化,也是错误的。const int i = 3;
-
数据类型
- 数据:常量和变量, 变量就是一块内存,每个变量都有确定的类型。
- 类型:决定了对变量能进行什么运算、变量占内存大小、变量值范围。
-
静态类型:类型检查
C++是一个静态类型语言,编译器会在编译程序时会根据变量类型检查是否支持相应的操作(运算),从而自动帮助我们发现程序bug。因此,编译时必须知道变量的类型才行。 -
类型表格
类型 含义 内存 示例 bool 布尔 char 字符 1字节 ’A’ wchart 宽字符 2字节 L’A’ char16t Unicode字符 2字节 u’A’ char32t Unicode字符 4字节 U’A’ short 短整型 2字节 3 int 整型 2字节 3 long 长整型 4字节 3L long long 长长整型 8字节 3LL float 单精度浮点型 4字节 3.14e-2f double 双精度浮点型 8字节 3.14,-3.14e3 long double 长双精度浮点型 10字节 3.14L -
类型别名:typedef 和 using
typedef 类型名 新类型名;
using 新类型名=类型名 ;typedef unsigned long ulong; using ushort = unsigned short;
-
格式化输出
std::dec:后续的整数都以十进制形式输出。
std::hex:后续的整数都以十六进制形式输出。
std::oct:后续的整数都以八进制形式输出。
std::fixed:以固定精度形式输出。
std::scientific:以科学计数法形式输出。
std::hexfloat:以十六进制浮点形式输出。
std::defaultfloat:以默认形式输出。
std::setw(n):改变输出域的宽度。
std::setprecision(n):改变浮点数的精度。
std::setfill(ch):改变填空字符,当setw的输出域宽度大于输出值的宽度时,默认的填空字符是空格,可以用setfill(ch)改变这个填空字符。 -
用户定义类型:允许程序员定义自己的数据类型
-
关键字enum class定义一个枚举数据类型。
enum class Day {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
定义了一个叫作Day的数据类型。这个枚举类型在定义时就列举出了这个类型所有可能的值,比如这个Day类型的变量只有7个可能的值。
-> 可以定义该类型Day的变量:Day d{Day::Tuesday};
-
不同枚举类型的值是不能相互比较或赋值的。
enum class Color {red, green, blue}; enum class Color2 {res, green, blue, yellow}; int main() { Color c = Color::green; Color2 c2 = Color2::red; c2 = c; //错误用法 }
-
-
C++标准库的字符串类型:string
#include <iostream> #include <string> using std::string; using std::cout; using std::endl; int main() { string s1("hello"), s2 = " world", s3; s3 = s1 + s2; cout << s3 << " size of s3 is " << s3.size() << endl; return 0; }
-
类class
程序员可以定义自己的类(class)。#include <iostream> #include <string> using std::string; using std::cout; using std::endl; struct Person { string name; int age; }; int main() { Person john; john.name = "john"; john.age = 30; cout << john.name << "\t" << john.age << endl; return 0; }
-
作用域和生命期
- 全局变量的作用域是整个程序。其生命期是整个程序的生命期
- 局部变量的作用域是从它定义的地方开始,到它所在的程序块的结尾。超出它所在的程序块,这个变量就不存在了,当然就不能访问。
-
复合类型:数组、指针和引用
- 引用:
- 引用(Reference)就是一个变量(对象)的别名。
T& ref{var}; //定义了引用变量ref,它是变量var的别名(引用)
- 定义引用变量时可以用不同的初始化方式:
T& ref = var; int ival{1024}; int &ref{ival}; //int 类型的引用变量ref是变量ival的别名
- 对引用变量的操作就是对它引用的那个对象的操作。
int ival{1024}; int &ref{ival}; //ref引用ival,是ival的别名 ref=24; //也就是ival = 24,因为ref和ival是同一块内存的不同名字而已 int ii{ref}; // 相当于 ii{ival} int &ref3{ref}; // 相当于int& ref3 = ival,即ref3和ref一样都是ival的别名
- 一个语句里可以定义多个引用。
int i{1024}, i2{2048}; int &r{i},r2{i2}; //r引用i,r2是普通int变量,不是引用 int i3{24} ,&ri{i3}; //i3不是引用,ri引用i3 int &r3{i3},&r4{i2}; //r3引用i3,r4引用i2
- 引用变量必须引用一个变量(对象),而不能是文字量,另外,引用变量类型和被引用变量类型应该一致。
int &ref4{10}; //错: 不能引用文字量 double dval{3.14}; int &ref5{dval}; //错:引用变量类型和被引用变量类型不一致
- 引用变量一旦定义,就不能再引用其他变量。即不能“重定义”。
int a,b; int &ra{a}; int &ra{b}; //错: 不能重定义同一个引用变量ra
- 引用(Reference)就是一个变量(对象)的别名。
- 引用:
-
指针
-
指针类型
对于一个类型T,T* 是T指针类型。即T*类型的变量可以保存T类型变量的地址。char c = 'a'; char *p = &c; // p的类型是char *,它初始化为char类型变量c的地址(指针) // 其中&是取地址运算符,用于获得一个变量(比如c)的地址(指针)
&是取地址运算符,它作用于一个变量,可以得到这个变量的地址。
- T *和T是完全不同的两个类型,相互之间不能初始化或赋值。
char *q {c}; //不能用char类型的值初始化char*类型的变量 p = c; //也不能将char类型的值赋值给char*类型的变量 char ch{p}; //char类型变量也不能用char*类型值初始化 ch = p; //也不能用char*类型值赋值给char变量
- 解引用运算符 * 作用于指针变量p,得到它指向的那个变量。
char c{'a'}; char *p{&c}; //p存储的是c的地址,即p指向c。 *p = 'A'; // *p就是c,相当于 c = 'A';即变量c的内存块存储的内容是字符'A' char c2{*p}; //相当于 char c2 {c}。即c2的初始值就是c的值,即字符'A'
- 指针变量也占据一块独立的内存。因此,定义指针变量时,不一定要初始化。而引用变量仅仅是其他变量的别名,引用变量本身不占据单独的一块内存,引用变量定义时则必须初始化。
double *s; //指针变量定义时可以不初始化 double &r; //错!引用变量定义时必须指明引用哪个变量!
- 给指针变量初始化和赋值时,类型须相同或能隐含转换。
- *和&的多个含义
定义变量时,*和&分别表示定义指针变量和引用变量;
作为运算符时,*和&则分别表示解引用和取地址运算符。int i{56}; int &r{i}; //r引用i,即r是变量i的别名,r和i是同一块内存的不同名字 int *p; //p的类型是int*,可存储int型变量的地址 p = &i ; //&i得到int型变量i的地址,赋值给变量p。两者类型都是int * *p = 3; // *p得到p指向的那个变量,即i。因此这句命令相当于: i = 3; int &r2{*p} ; // int型引用变量r2引用的变量是*p,即i
- T *和T是完全不同的两个类型,相互之间不能初始化或赋值。
-
nullptr只能初始化指针变量
- 不能赋值给非指针类型的变量。
int *pi{nullptr}; double *pd{nullptr}; int ival{nullptr}; //错:ival不是一个指针变量
- 不能用整数给指针赋值,即使这个整数为0, 因为类型不同。
int zero{0} ,*p1; p1{zero}; //错:p1类型是int*,而zero类型是int, int *p2{2}; //错:p2类型是int*,而2的类型是int
- 不能赋值给非指针类型的变量。
-
指针的其他运算
- 和非0值一样,非空指针可以自动转化为bool类型的值true,和0一样,空指针可以自动转化为bool类型的值false。
int i; int *p{&i},*q{0}; bool b{p}; // int*非空指针p转化为bool型值true // 然后对b初始化,因此,b的值是true std::cout << boolalpha << b <<std::endl; //boolalpha操作符控制bool量的显示形式 b = q; // int *空指针q转化为bool型值false // 然后赋值给b,因此,b的值是false; std::cout << boolalpha << b <<std::endl;
- 指针类型的变量可以用比较运算符(!= 、==、>=、<等)比较大小或是否相等。结果是一个逻辑值true或false。
std::cout << boolalpha << (p!=q) <<std::endl;
- 指针可以和整数进行加减运算,用于对指针进偏移(在数组和动态内内存时会再进一步介绍)。
- 和非0值一样,非空指针可以自动转化为bool类型的值true,和0一样,空指针可以自动转化为bool类型的值false。
-
void* 无类型指针
- void* 变量可以存储任何内存地址(任何类型对象的地址)
- void* 变量之间可以比较大小,可以相互赋值。
- void* 变量不能*解引用、不能对指针进行偏移、不能隐式类型转换到非void指针类型,但可以通过强制类型转换staticcast将它转换到特定的指针类型。
- void *指针变量主要用于将不同类型的指针变量传递给函数,在函数内部再将它强制转换为特定的指针类型。
int main() { int * pi; void * pv = pi; // ok: int*到 void*的隐式类型转换 *pv; //错 :不能解引用 void* ++pv; //错: 不能增量或偏移 void* (指向对象的内存大小未知) int* pi2 = static_cast<int*>(pv); // void* 强制类型转换到int* double* pd1 = pv; // 错:不能将void*初始化或赋值给非void*指针变量 double* pd2 = pi; // 错:指针类型不一致 double* pd3 = static_cast<double*>(pv); // 不安全 }
-
指针的指针
- 既然指针变量pi也是占据独立内存块的变量,它本身的地址&pi也可以保存在一个指针变量ppi中,这个指针变量ppi通常称为指针的指针,也就是说ppi存储的是一个指针变量的地址。
#include <iostream> using namespace std; int main() { int ival{1024}; int *pi{&ival}; //pi 存储ival的地址 int **ppi{&pi}; // ppi存储pi的地址。 pi的类型是int * // 所以&pi的类型 (int *) *,即 int **,int **就是(int *)* // ppi ---> pi -->ival cout << "ival的值是: " << ival << endl; cout << "ival的值是: " << *pi << endl; // *pi就是ival cout << "ival的值是: " << **ppi <<endl; // **ppi即 *(*ppi),而*ppi就是pi, // 因此,**ppi就是 *(pi)即ival cout << "\nival的地址是:" << &ival << endl; cout << "ival的地址是:" << pi << endl; //pi保存的是ival的地址 cout << "ival的地址是:" << *ppi << endl;//*ppi就是pi cout << "\npi的地址是:" << &pi << endl; cout << "pi的地址是:" << ppi << endl; //pi保存的是ival的地址 return 0; }
ival的值是: 1024 ival的值是: 1024 ival的值是: 1024 ival的地址是:0x7ffe70b356f4 ival的地址是:0x7ffe70b356f4 ival的地址是:0x7ffe70b356f4 pi的地址是:0x7ffe70b356f8 pi的地址是:0x7ffe70b356f8
- 既然指针变量pi也是占据独立内存块的变量,它本身的地址&pi也可以保存在一个指针变量ppi中,这个指针变量ppi通常称为指针的指针,也就是说ppi存储的是一个指针变量的地址。
-
指针的引用
-
指针既然是一个占有独立内存的变量(对象),当然可以定义一个引用它的引用变量,即给它起一个引用别名。
int i{42}; int *p; int *&r{p}; //r引用p,即r是p的别名 r = &i; //将i的地址赋值给r,也就是p,因此p指针变量里保存的就是i的地址 *r = 0 ; //相当于*p = 0,p指针变量的值是0,即p成为一个空指针。
-
下面用法是错误的:
int &*q; //错! 因为:从右向左看,q是一个指针变量,存储的是int &变量的地址 // 也就是说q试图存储一个引用变量的地址,而引用变量是没有独立的内存块的 // 即引用变量没有地址!
-
-
引用和指针的比较
- 共同点:都是间接指向或引用其他对象
- 不同点: 引用(变量) 仅仅是其他变量的别名,无独立内存,同一个引用变量不能修改去引用不同的变量。指针变量存储其他变量的地址,有独立内存,在不同时刻可指向不同对象。
#include <iostream> using namespace std; int main() { auto i{0}, j{1}; int *p; // 指针变量不一定要初始化 int &r{i}, &r1; // 错:引用变量r1没有初始化! p = &i; // p指向i p = &j; // p指向j auto *&rp{p}; // rp引用p int *&rp2; // 错:引用变量rp2没有初始化 int &*q; // 错:不能定义指向引用的指针,因为引用变量没有独立内存(即没有地址) int &*q2 = &r; // 错:原因同上。另外,取地址运算符&不能作用于引用变量r! return 0; }
-
-
数组
- 不能用一个数组去初始化或赋值给另一个数组
- 字符数组可以用一个字符串文字量进行初始化
- 复杂数组:
int *ptrs[10]; //10个int*元素的数组 int (*parr)[10]; //parr是一个指针,指向的是int[10]的数组, //即指向的是10个int元素的数组, //或者说它存储的是int[10]数组的地址 parr = &arr; //将 int[10]类型数组arr的地址赋值给 parr parr = &ar; //错:类型不一致! ar的类型是 int[3]而不是int[10]。
- 正如定义指针变量parr指向一个数组arr一样,也可以定义1个引用变量引用一个数组
int (&ref_arr)[10] = arr; // ref_arr是一个引用变量,引用的是10个int元素的数组,而arr正好是10个int的数组 int (&ref_arr)[10] = ar; // 错:ref_arr是一个引用变量,引用的是10个int元素的数组,而ar是3个int的数组。类型不一致!
-
指针访问数组
- 数组名是指向数组第一个元素的指针(地址)
#include <iostream> using std::cout; using std::endl; int main() { int v[]{1, 2, 3, 4}; int *p1{&(v[0])}; // p1存储的是第一个元素v[0]的地址 int *p2{v}; // 数组名就是数组的第一个元素的地址,等价于int *p2{&(v[0])}; int *p3{v + 4}; // v是第一个元素的指针,向后偏移4个int元素空间。 cout << *(p1 + 2) << endl; // *(p1+2)等价于*(v+2),该语句输出第3个元素 return 0; }
- 用下标访问数组元素实际上在编译过程中,会转化成这种指针偏移。对整型变量j,下列访问数组元素的式子都是等价的:
v[j] == *(&(v[0])+j)== *(v+j) == *(j+v) ==j[v]
- 对于一个指针变量p和一个整数n,除了可以用 p+n、p-n 、p+=n 、p-=n等算术运算对指针进行偏移外,也可以用自增(p++或 ++p)、自减(p–或–p)进行偏移。
#include <iostream> using std::cout; using std::endl; int main() { int v[] = {1, 2, 3, 4}; int *p = v; p[2] = 20 + *(v+3); int b = *(p+2), c = v[2], d = *(v+2); p ++; // p从v向后偏移1个int占据的空间(4个字节),即p指向v[1]; p ++; // p指向v[2] cout << b << '\t' << c << '\t' << *p << endl; p -= 2; // p向前偏移2个int占据的空间,即地址减去了8个字节,p指向了第一个元素 cout << *p << endl; return 0; }
24 24 24 1
- 2个指针不能相加,但指向同一个数组的指针可以相减:表示两者之间的元素个数。
- 数组名是指向数组第一个元素的指针(地址)
-
动态内存
- 程序堆栈区
- 每个程序除了代码占据的内存外,都有一个称为堆栈(Stack)的内存块,用于存储程序块的非静态局部变量。
- 堆存储区:new和delete运算符
- 对于new T分配的一个T元素的内存,用delete p释放p指向的这块T元素占据的内存。对于new T[size]分配的多个T元素空间的内存,用delete[] p释放p指向的多个T元素占用的内存,如果写成了delete p释放的将是第一个T元素占用的内存,其他元素的内存并没有得到释放,这会造成内存泄漏。
- 动态内存表示多维数组
#include <iostream> using std::cin; using std::cout; using std::endl; int main() { int n = 0; // 学生人数 int cols; // 每个学生的成绩个数 cin >> n; auto scores{new double[n][4]}; cout << "输入学生的平时、实验、期末、总评成绩\n"; for (auto i = 0; i != n; i++) { cin >> scores[i][0] >> scores[i][1] >> scores[i][2] >> scores[i][3]; } for (auto i = 0; i != n; i++) { cout << scores[i][0] << '\t' << scores[i][1] << '\t' << scores[i][2] << '\t' << scores[i][3] << endl; } return 0; }
- 程序堆栈区
-
(成员访问运算符.);(间接访问运算符->);(取内容运算符*)
-
this 指针
- 类的(非静态)成员函数都有一个this隐含参数,它指向调用这个函数的那个对象
#include <iostream> using std::cin; using std::cout; using std::endl; using std::string; struct student { string name; double score; void print() { cout << this->name << " " << this->score << endl;} // void print(student *this) { cout << this->name << " " << this->score << endl;} }; int main() { student stu, stu2; stu.name = "Li"; stu.score = 67.8; stu2.name = "Wang"; stu2.score = 77.5; stu.print(); // void print(&stu); stu2.print(); // void print(&stu2); return 0; }
- 构造函数:
- 在定义类对象的时候,编译器会自动调用一个特殊的叫做构造函数的成员函数对类对象的数据成员初始化。
- 没有定义构造函数,则编译器会自动生成一个参数列表和函数体都为空的默认构造函数。
#include <iostream> using std::cin; using std::cout; using std::endl; using std::string; class Date { int year{ 200 }, month{ 1 }, day{ 1 }; public: void print() { cout << year << '-' << month << '-' << day << endl; } }; int main() { Date day, day1; // day和day1都用默认构造函数构成 day.print(); day1.print(); return 0; }
- 构造函数:函数名和类名相同,没有返回类型
- 默认构造函数:不带参数、或参数都有默认值
#include <iostream> using std::cin; using std::cout; using std::endl; using std::string; class Date { int year{ 200 }, month{ 1 }, day{ 1 }; public: Date() { cout << "构造Date对象:" << endl; print(); } void print() { cout << year << '-' << month << '-' << day << endl; } }; int main() { Date day; // 定义类对象时自动调用匹配的构造函数 return 0; }
- 构造函数:可以带参数
- 如果定义了构造函数,则编译器就不会再生成默认构造函数
- 可以通过default关键字来通知编译器生成默认构造函数
Date() = default;
- 类的(非静态)成员函数都有一个this隐含参数,它指向调用这个函数的那个对象
-
初始化成员列表
- 对于构造函数,可以在函数体前面对类的数据成员进行初始化
- 避免了“在进入构造函数前先默认初始化类成员,然后在构造函数体里再对这些成员重新赋值”,而直接用构造函数的参数对类对象的成员初始化一次,函数体中不再重新初始化。
Date(int y = 2000, int m = 1, int d = 1): year{y}, month(m), day(d) {}
- 类成员如果是non-static const、引用变量、无默认构造函数类对象,则无法在构造函数体内初始化,必须用初始化成员列表
#include <iostream> using std::cin; using std::cout; using std::endl; using std::string; class X { const int a; public: X(int a) : a(a) {} int get() { return a; } }; int main() { X X(10); cout << X.get(); cout << endl; return 0; }
-
拷贝构造函数
- 用已存在的同类对象对该类对象初始化。假如有一个类X的对象x,则可以用x去初始化一个新的X类对象:
X y{x}; //也可以写成X y(x);
- day和day2对象具有完全一样的数据成员值。在定义day2对象时传递的是day对象,产生的day2对象是day对象的复制(拷贝)。
int main() { Date day{ 2018, 1, 1 }, day2{ day }; day.print(); day2.print(); }
- 默认拷贝构造函数
- 如果没有定义拷贝构造函数,编译器会自动生成一个默认拷贝构造函数
- 对于一个类X,拷贝构造函数的函数规范是X(const X& x),即其参数是一个该类的const对象的引用
- 对于上面的Date类,编译器默认生成的拷贝构造函数是:
Date(const Date& d) : year{ d.year }, month(d.month), day(d.day) {}
- 用已存在的同类对象对该类对象初始化。假如有一个类X的对象x,则可以用x去初始化一个新的X类对象:
-
赋值运算符:operator=
- 可以将一个类对象赋值给另外一个类对象(和拷贝构造新对象复制已有对象的区别是,赋值运算符是在2个已经存在的对象之间的复制(拷贝),而拷贝构造函数是用已有对象创建一个新对象。)
Date day { 2018, 1, 1 }, day3; day3 = day; day.print(); day3.print();
- 对于类类型X,赋值运算符函数operator()=类似于拷贝构造函数
X& operator = (const X& object);
- 和拷贝构造函数不一样的,赋值运算符函数返回对象自身的引用
class X { X& operator = (const X& object) { return *this; } };
- 示例:
#include <iostream> using std::cout; using std::endl; class Date { int year{ 2000 }, month{ 1 }, day{ 1 }; public: Date(const Date& d): year{ d.year }, month(d.month), day(d.day){} Date(int y = 2000, int m = 1, int d = 1): day(d), month(m), year{ y }{} void print() { cout << year << '-' << month << '-' << day << endl; } Date& operator=(const Date& date) { cout << "赋值运算符" << endl; this->year = date.year; this->month = date.month; this->day = date.day; return *this; // 返回被赋值对象的而自身的引用 } }; int main() { Date day(2018, 1, 1); Date day2, day3; day3 = day2 = day; // 先执行day2 = day,结果是day2,再执行day3 = day2 // day2 = day实际是day2.operator=(day) }
- 可以将一个类对象赋值给另外一个类对象(和拷贝构造新对象复制已有对象的区别是,赋值运算符是在2个已经存在的对象之间的复制(拷贝),而拷贝构造函数是用已有对象创建一个新对象。)
-
private、public关键字说明类的成员是否可以被外界访问
-
关键字protected修饰的成员称为保护成员,外界也是无法访问的,只能被该类和从该类派生的派生类的方法访问。
-
如果一个对象销毁时没有释放其占用的内存,会导致这块内存无法被其他程序或该程序的其他代码使用,即导致内存泄漏。
-
运算符重载
- 对用户定义类型,重新定义运算符函数,称为运算符重载。
- 运算符重载的2种方式
- 成员函数:将运算符函数定义为类的成员函数。
#include <iostream> using std::cout; using std::endl; using std::string; class Point { double x{}, y{}; public: Point(double x, double y): x{ x }, y{ y }{} Point operator+(const Point& other) { return Point(x + other.x, y + other.y); } friend void print(const Point& p); }; void print(const Point& p) { cout << p.x << ',' << p.y << endl; } int main() { Point P{ 2, 3 }, Q{ 4, 5 }; print(P + Q); return 0; }
- 外部函数:将运算符函数定义为外部函数(全局函数)。
#include <iostream> class Point { double x{}, y{}; public: Point(double x, double y) : x{ x }, y{ y } {} friend Point operator+(Point P, const Point& other); friend void print(const Point& p); }; Point operator+(Point P, const Point& other) { return Point(P.x + other.x, P.y + other.y); } void print(const Point& p) { std::cout << p.x << "," << p.y; } int main() { Point P{ 2, 3 }, Q{ 4, 5 }; print(P + Q); return 0; }
- 成员函数:将运算符函数定义为类的成员函数。
- 下标运算符通常定义2个版本
- 一个是返回可以被修改的引用,即可以作为赋值运算符的左操作数。T& operator[](int i) ;
- 一种是const成员函数,返回的是一个值,可用作赋值运算符的右操作数。T operator[](int i) const;
#include <iostream> class Point { double x{}, y{}; public: Point(double x, double y) : x{ x }, y{ y }{} double& operator[](int i) { if (i == 0) return x; else if (i == 1) return y; else throw "下标非法"; } double operator[](const int i) const { if (i == 0) return x; else if (i == 1) return y; else throw "下标非法"; } friend void print(const Point& p); }; void print(const Point& p) { std::cout << p.x << "," << p.y; } int main() { Point P{ 2, 3 }; P[0] = 4; // P[0]调用的是引用版本 P[1] = P[0]; // P[1]调用的是引用版本,P[0]调用的是const版本 return 0; }
- 输入输出运算符
#include <iostream> class Point { double x, y; public: Point(double x = 0, double y = 0) : x{ x } , y{ y }{} friend std::ostream& operator<<(std::ostream& out, const Point& p); }; std::ostream& operator<<(std::ostream& out, const Point& p) { out << "(" << p.x << "," << p.y << ")"; return out; } int main() { Point P; std::cout << P; return 0; }
- 比较运算符: <、>、<=、==、…
#include <iostream> class Point { double x, y; public: bool operator<(const Point& other); bool operator==(const Point& other); bool operator<=(const Point& other) { return *this < other || *this == other; } }; bool Point::operator<(const Point& other) { if (x == other.x) return y < other.y; return x < other.x; } bool Point::operator==(const Point& other) { return x == other.x && y == other.y; }
- 有的运算符只能作为外部函数重载,如:new、new[]、delete、delete[]
- 有一些运算符不能被重载,如::: 作用域运算符 . 成员访问运算符 .* 成员选择运算符 ?:条件运算符 sizeof 查询对象的大小 typeid查询对象的类型
-
派生类
- 派生类可访问基类的protected成员
- 二义性问题:如果一个派生类的不同基类包含了同名的数据成员或同样签名的函数成员,当通过该派生类对象访问这个成员时,可能会产生二义性问题。
#include <iostream> class USBDevice { private: long m_id; public: USBDevice(long id) : m_id(id) {} long getID() { return m_id; } }; class NetworkDevice { private: long m_id; public: NetworkDevice(long id) : m_id(id) {} long getID() { return m_id; } }; class WirelessAdapter : public USBDevice, public NetworkDevice { public: WirelessAdapter(long usbId, long networkId) : USBDevice(usbId), NetworkDevice(networkId) { } }; int main() { WirelessAdapter wa(5442, 181742); std::cout << wa.getID(); // 调用哪一个getID()? return 0; }
- dynamiccast
- dynamiccast<>主要用于具有多态性的层次继承结构的类之间的指针(或引用)的向上、向下和侧向转换。它是在程序运行期间根据指针(或引用)指向(或引用)的对象的实际类型确定是否能安全地进行指针(或引用)类型的转换。其格式是:
dynamiccast<Type*>§
dynamiccast<Type&>®
即在运行时,将指针p(或引用r)转换为类型Type* (或Type& )。
如果不能进行类型转换,对于指针,返回空指针,对于引用,则抛出一个异常(错误)。 - dynamiccast<>主要用于将一个基类指针(或引用)转换为一个派生类的指针(或引用),即向下类型转换(downcasting)。向上类型转换(upcasting)可以使用也可以不使用dynamiccast。
#include <iostream> using std::cout; struct Base { virtual ~Base() {} }; struct Derived : Base { virtual void name() {} }; int main() { Base* b1 = new Base; if (Derived* d = dynamic_cast<Derived*>(b1)) { std::cout << "downcast from b1 to d successful\n"; d->name(); // safe to call } Base* b2 = new Derived; if (Derived* d = dynamic_cast<Derived*>(b2)) { std::cout << "downcast from b2 to d successful\n"; d->name(); // safe to call } delete b1; delete b2; }
#include <iostream> struct V { virtual void f() {}; //必须是多态的才能使用运行时检查的dynamic_cast }; struct A : virtual V {}; struct B : virtual V { B(V* v, A* a) { // 构造过程中的类型转换(看下面D的构造函数的调用) dynamic_cast<B*>(v); // 没问题: v的类型是 V*, 而V是B的基类,v可以转化为B*类型 dynamic_cast<B*>(a); // 不可预知:undefined behavior: a的类型是A*, // 但A不是B的基类 } }; struct D : A, B { D() : B(static_cast<A*>(this), this) { } }; int main() { D d; A& ra = d; // 向上类型转换:派生类引用转化为基类引用。可以使用也可以不使用dynamic_cast D& rd = dynamic_cast<D&>(ra); // 向下类型转换:基类引用转化为派生类引用 B& rb = dynamic_cast<B&>(ra); // 侧向类型转换:从A&转换为B& A a; D& rda = dynamic_cast<D&>(a); //运行时错误:因为实际对象a不是D类型 B& rba = dynamic_cast<B&>(a); //运行时错误:因为实际对象a不是B类型 }
- dynamiccast<>主要用于具有多态性的层次继承结构的类之间的指针(或引用)的向上、向下和侧向转换。它是在程序运行期间根据指针(或引用)指向(或引用)的对象的实际类型确定是否能安全地进行指针(或引用)类型的转换。其格式是:
- override
在派生类的虚函数签名后添加override关键字,说明这是一个从基类继承下来的虚函数,编译器会检查基类是否有这个虚函数,如果没有就会报告错误。 - final
虚函数签名后添加final关键字。表示虚函数的继承到此为止,其派生类不能再定义或继承该虚函数了。
-
模板
#include <iostream> template<typename T> T Max(T a, T b) { return a > b ? a : b; } int main() { int x = 10, y = 20; int* p = &x, * q = &y; std::cout << *Max(p, q) << std::endl; return 0; }