C++
一、C和C++的区别
- 语言性质(最大的区别)
面向过程编程:在实现一定功能的过程中将功能拆分成一个个任务,并用若干书写形式互相独立的函数去完成任务,分工明确,可读性强,可复用性差,数据安全性差
面向对象编程:引入了类
泛型编程:发明一种语言机制可以实现一个通用的标准容器库,将数据类型作为参数传递。最开始是C++的STL标准模版库
就面向过程而言,C和C++几乎差不多,但C++还支持面向对象和泛型编程。
- C++允许自定义命名空间
当使用iostream头文件的时候,该头文件没有定义全局命名空间,必须使用C++所规定的标准的命名空间std
using namespace std;
//去掉这一行会报错error: use of undeclared identifier 'cout'; did you mean 'std::cout'?
namespace A{
string a = "hello, this is A";
}
namespace B{
namespace C{
string a = "hello, this is C";
int b = 3;
}
}
int main(){
cout << A::a << endl;
cout << B::C::a << endl;
cout << B::C::b << endl;
B::C::b = 4;
cout << B::C::b << endl;
return 0;
}
-
关键字上:C++在动态内存管理上增加了new/delete,增加了auto类型
-
C++增加了引用的概念
-
C++支持函数重载(函数的名字修饰带来的影响),引入虚函数用以实现多态
-
对于struct:C++增加了成员函数、成员变量、访问权限等概念
-
__STDC判断是否为C、C++ 、cplusplus内置宏判断是否为C++
-
…
二、内存分布情况
内存由堆区、栈区、静态区、常量区、代码区组成。堆区和栈区一般被称为动态数据区。
- 栈区:由编译器管理,存放临时变量(局部变量、函数参数),函数退出后释放空间。增长方向是向着内存地址减少的方向,开口向下。
int main(){
int a,b;
printf("%p\n%p\n",&a,&b);
}
/*out
0x16db92ae8
0x16db92ae4
*/
- 堆区:由程序员管理,动态内存分配(new,malloc、delete、free),如果没有及时释放会造成运行的程序出现内存泄漏的错误。增长方向是向着内存地址增加的方向,开口向上。
注:内存泄露指的是程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
int main(){
int* a = new int;
int* b = new int(2);
printf("%d %d\n",*a,*b);
printf("%d %d\n",a,b);
}
/*out
0 2
0x12ae06950 0x12ae06960
*/
- 全局/静态区:静态变量、全局变量,分为初始化和未初始化两个相邻的区域,程序结束后释放空间
- 代码区:存放程序的二进制代码
new和malloc的区别
- 自定义类型(最大的区别)
new先申请足够的内存,然后调用类型的构造函数,最后返回自定义类型指针。delete先调用析构函数,再释放内存
malloc/free是库函数,只能动态申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作
对于内部数据类型(例如int,float,char等)两者是等价的
class cat{
public:
cat(){x = 2;};
int x;
};
int main(){
cat *p1 = new cat();
cat *p2 = (cat *) malloc (sizeof(cat));
printf("%d\n", p1->x); //2
printf("%d\n", p2->x); //0
}
- 属性
new/delete是C++关键字,需要编译器支持,new的底层实现也是基于malloc的
malloc/free是C/C++库函数,在C中需要头文件<stdlib.h>或者<malloc.h>的支持
- 参数
new无需指定申请内存块的大小,编译器会根据申请的类型自动计算
malloc需要显式地指出申请内存的大小,一般会用malloc(sizeof(type))
- 返回类型
new分配成功时返回对象类型的指针,类型严格与对象匹配
malloc分配成功时返回void *,需要通过强制类型转换
- 重载
C++允许重载new/delete操作符,malloc不允许重载
- 内存区域
一个是自由存储区,一个是堆上。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new进行内存申请,该内存就是自由存储区。自由存储区不一定在堆上,可以通过重载new操作符,改用其他内存区域实现动态存储。
注:new/delete的功能似乎包含了malloc/free,为什么不只用前者?这是由于C++会经常调用C函数,用delete去释放malloc申请的空间可能导致程序出错。因此仍保留,需要使用的时候就成对使用。
三、指针
存储一个存储器地址,这个地址的值直接指向该地址上的对象的值,可以为空,sizeof得到的是指针类型的大小
野指针:没有经过初始化的指针。
悬挂指针:最初指向的内存被释放后,未被置空的指针。
如何避免?初始化、及时把指针置空、智能指针。
四、引用
是C语言的一个重要扩充,类似于windows的快捷方式,通过该别名能够找到同一份数据
不可以为空,必须在定义的同时初始化,不改变指向,更安全。
sizeof得到的是初始化对象的大小
常引用:不希望通过引用修改原始数据
type& name = data;
const type& name = data;
int a = 4;
int& r = a;
cout << a << " " << r << endl;
cout << &a << " " << &r << endl;
/*out
4 4
0x16d18eae8 0x16d18eae8 =====》 绑定在同一块内存
*/
作为函数参数
绑定实际参数和形式参数,如果在函数体中修改了形式参数的值,能够同时影响实参的值,从而拥有“在函数内部影响外部数据”的效果
void addbyone(int& x){
x += 1;
}
int main(){
int a = 4;
addbyone(a);
cout << a << endl;
}
//out 5
作为函数返回值
int arr[5] = {1,2,3,4,5};
int& setvalue(int i){
int& p = arr[i];
return p;
}
int main(){
for (int i=0; i<4; i++) printf("%d ",arr[i]); printf("\n");
setvalue(0) = 100;
setvalue(3) = 500;
for (int i=0; i<4; i++) printf("%d ",arr[i]); printf("\n");
}
/*out
1 2 3 4
100 2 3 500
*/
五、变量
作用域 | 内存中的存放位置 | 初始 | |
---|---|---|---|
全局变量 | 整个工程文件内都有效 | 全局区 | 0 |
静态全局变量 | 只在定义它的文件内有效 | 静态区 | |
局部变量 | 在定义它的函数内有效,函数返回后失效(如果想将函数内的值重复利用而不是在函数退出时立即死亡 => 静态) | 栈区 | 随机 |
静态局部变量 | 只在定义它的函数内有效,且程序仅分配一次内存,函数返回后,该变量不会消失 | 静态区 |
静态变量
起到控制变量的存储方式和作用域
修饰局部变量:栈区 => 静态区,定义它的函数内返回后失效 => 延续到程序结束后才失效,作用域都是定义它的函数内
修饰全局变量:全局区 => 静态区,能够被同一个工程内的其他源文件所访问 => 只能在定义的本文件内访问到,生命周期相同
修饰函数:与全局变量相同,修改了作用域
修饰类:在C++中,静态成员属于整个类而不是某个对象,静态成员变量只存储一份供所有对象共用。所以在所有对象中都可以共享它。使用静态成员变量可以实现多个变量之间共享数据不会破坏隐藏的原则,保证了安全性还可以节省内存。静态成员的定义或声明要加个关键 static。静态成员可以通过双冒号来使用即 <类名>::<静态成员名>
class node
{
public:
static void stt(){printf("%d\n",1);};
void stt2(){printf("%d\n",2);};
};
int main(int argc, char *argv[])
{
node::stt();// 1
node::stt2();// call to non-static member function without an object argument
}
常量
把常量定义为大写字母形式,是一个很好的编程实践。
与普通变量不同,常量必须进行初始化!!!
- #define宏定义
#define identifier value
#define PI 3.14159
- const关键字
修饰指针的三种方式:
int a = 5;
int* p = &a;
const int* p = &a; //指针指向可以改,但指向的内存单元的值不可以修改
int* const p = &a; //指针指向不可以改,...可以改
const int* const p = &a; //两者都不可以改
但是并不是意味着该a无法进行一切修改,只是无法通过指针变量p对其进行修改。
修饰函数参数:接受常量,函数体内不会改变其参数,保护原对象的变量不会被函数改变。
修饰类:定义常量对象,只能调用常量函数
- 区别
1.编译器处理:
编译器在预处理阶段把宏展开,不能对宏进行调试
编译器在编译阶段处理const常量,类似于一个只读数据
2.存储方式:
宏定义是直接替换,没有内存分配,在编译后就不存在了
const常量有内存分配,存储在内存的常量区
3.类型和安全检查:
宏定义的对象没有数据类型,只是进行字符替换,编译器不会对其进行类型安全检查
const定义的对象有数据类型,编译器会进行类型安全检查
六、类和对象
创建
实例化:通过类名创建对象的过程
类有点类似于一个数据类型,编译后不占用内存空间,只有在创建对象后才会给对象及它的成员变量分配内存,并为它们赋值。
在栈上:Student stu; 使用.访问成员
在堆上:Student *pStu = new Student; 这时候必须使用指针,该对象相当于是匿名的, 使用箭头访问成员
对象的内存模型:所有成员函数存放在代码区。对于不同的对象,成员变量可能不同,但成员函数代码一定都是相同的,这样做节省内存空间。虚函数的地址存放在该对象关联的虚表中,而其他函数地址通过编译器获取
成员
-
成员变量(属性)
-
成员函数(方法)
成员函数与一般函数的区别是:有权限的限制,由类决定
如果在类外定义成员函数时,需要先在类中作原型声明,所以类的位置需要定义在类外的函数之前
::符号是域解析符
class Student{
public:
int age = 10;
void print(int x);
};
void Student::print(int x){
printf("%d %d\n",this->age,x);
}
定义在类内的函数默认为内联函数(减小函数调用的时空开销, C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,(解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题) 类似于C语言中的宏展开),定义在类外的需要加上inline关键字
- 成员对象(嵌套)
类内有成员对象的类称为封闭类
实例化时先调用成员对象的构造函数,再调用该类的构造函数
析构时先调用该类的析构函数,再调用成员对象的构造函数
class Member{
public:
Member(){std::cout << "Member构造函数" << std::endl;}
~Member(){std::cout << "Member析构函数" << std::endl;}
};
class Test{
private:
Member member;
public:
Test(){std::cout << "Test构造函数" << std::endl;}
~Test(){std::cout << "Test析构函数" << std::endl;}
};
int main()
{
Test* test = new Test();
delete test;
}
/* out
Member构造函数
Test构造函数
Test析构函数
Member析构函数
*/
类的作用域
每个类都会定义自己的作用域,在类的作用域之外,普通成员只能通过对象去访问,静态成员可以通过对象或者类访问,typedef定义的类型只能通过类来访问
class Student{
private:
char* m_name;
public:
typedef char* PCHAR;
Student(char* name):m_name(name){};
char* show(Student* p);
};
Student::PCHAR Student::show(Student* p){
cout << "sss" << p->m_name << endl;
return m_name;
}
this指针
this是C++的一个关键字,也是一个const指针,指向当前对象,但只能在类的内部,访问所有类内的所有成员(类内的成员都是可以互相访问的)
普通成员函数都是有this指针的,且只能在成员函数内部使用,否则非法
void Student::printThis(){
cout<<this<<endl;
}
Student *pstu1 = new Student;
pstu1 -> printThis();
cout << pstu1 << endl;
/* out
0x7b17d8
0x7b17d8
*/
静态成员变量
含义:所有的对象都共享这些静态成员变量,都可以引用它
static成员变量必须在类声明的外部进行初始化,不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存
可以通过类或对象或对象指针来访问
static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。例如,假设现在没有学生,则初始化后Student::m_total = 0,也是占有内存的,反之如果没有在类外进行初始化的静态成员变量是不可以被使用的
class{
private:
static int m_total;
char *m_name;
int m_age;
float m_score;
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
m_total++; //操作静态成员变量
}
};
int Student::m_total = 0;
int main(){
Student::m_total = 10; //通过类类访问
Student stu("小明", 15, 92.5f);
stu.m_total = 20; //通过对象来访问
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20; //通过对象指针来访问
}
静态成员函数
编译器在编译普通成员函数时会隐式地增加一个this指针,并把当前对象的地址赋给this,因此它可以访问所有成员(包括成员变量和成员函数),但它需要有当前对象的地址,在创建一个对象后才可以调用普通成员函数
最本质区别:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)
#include<iostream>
using namespace std;
class Student{
private:
char* m_name;
float m_score;
static int m_total_num;
static float m_total_score;
public:
Student(char* name, float score):m_name(name),m_score(score){
m_total_num += 1;
m_total_score += score;
}
static int allNum(){return m_total_num;}
static float allScore(){return m_total_score;}
};
int Student::m_total_num = 0;
float Student::m_total_score = 0.0;
int main(){
Student* x1 = new Student("lihua",90.0);
Student* x2 = new Student("sha",100.0);
cout << x2->allNum() << endl;
cout << x2->allScore() << endl;
}
常成员和常对象
常成员函数可以使用类中的所有成员变量,但是不能修改它们的值,保护数据而设置的
通常是getname这种只是为了获取变量的值,保险,并可以让语义更明显
常对象只能调用类内的常成员变量和常成员函数
class Student{
public:
char* m_name;
char* getname() const;
Student(char* name):m_name(name){}
};
char* Student::getname() const{
this->m_name = "lihua2";//member function 'Student::getname' is declared const here
return this->m_name;
}
int main(){
Student tmp("lihua");
cout << tmp.getname() << endl;
}
成员权限
权限只能修饰类的成员,C++中的类没有共有和私有之分,且在类的内部,都是可以相互访问的
public公共权限(struct默认权限),类外可以访问
protected保护权限,类外不可以访问 儿子可以访问
private私有权限(class默认权限),类外不可以访问 儿子不可以访问
class Student{
private:
char* m_name;
int m_age;
public:
Student(const Student& x){
m_name = x.m_name;
m_age = x.m_age;
}
Student(){};
void print();
void resetName(char* name);
void resetAge(int age){m_age = age;}
};
void Student::print(){
cout << this->m_name << endl;
cout << this->m_age << endl;
}
void Student::resetName(char* name){
this->m_name = name;
}
int main(){
Student stu;
stu.resetName("lihua");
//stu.m_name = "lihua"; error: 'm_name' is a private member of 'Student'
stu.resetAge(14);
stu.print();
Student stu1(stu);
stu1.resetName("sh");
stu1.print();
}
将成员变量声明为 private、将部分成员函数声明为 public 的做法体现了类的封装性。
用构造函数和类内的get、set函数对私有的成员变量进行读取或赋值,符合C++软件设计规范
所谓封装,指对客观事物封装成抽象的类,隐藏类的内部实现,自定义成员变量和操作这些成员变量的方法,对变量和方法加以不同的权限,控制类对象和外界对象对该类的访问。
函数编译原理和成员函数的实现
C++中的函数在编译时会根据命名空间、类等信息重新命名,形成新的函数名,这个算法过程称为名字编码。
编译成员函数时,如果函数体中没有成员变量,直接调用即可。反之,C++规定编译成员函数时要额外增加一个参数,把当前对象的指针传递进去,通过指针来访问成员变量,这对程序员来说是透明的(不知道它的存在),本质上并不是通过对象找函数,而是通过函数找对象
构造函数
C++规定与类同名的成员函数就是构造函数,用于创建类对象时初始化类对象成员变量,一旦创建类对象,构造函数自动被调用。可以有参数,没有返回值,可以重载,会根据声明时的参数选择合适的构造函数进行初始化。
-
有参构造
-
无参构造:如果没有自定义构造函数,编译器会提供一个默认的无参构造函数
Student(){}
- 拷贝构造:类型是当前类的引用,而且是const引用。如果不是当前类的引用,而是当前类的对象,则调用拷贝构造函数时,参数为这个对象,这本身就又是一次拷贝,会陷入无限循环。如果不是const引用,则不能用const类型来初始化类的对象了,这是因为非const类型可以转换为const类型,而const类型不能转换为非const类型,同时,这也与我们的初衷–“用其他对象的数据来初始化当前对象,其他对象的数据并不期望被改变”相符合。如果没有自定义拷贝构造函数,那么编译器会提供一个默认的拷贝构造函数
class node{
private:
string name;
int age;
public:
node(const node &x) {name = x.name;age = x.age;printf("copy is do!\n");};
node(string xname, int xage) :name(xname),age(xage) {printf("simple is do~\n");};
void pr(){
cout << name << endl;cout << age << endl;
};
};
int main(int argc, char *argv[]){
node tmp("xiaoming", 14);
node tmp2(tmp);
tmp.pr();
tmp2.pr();
}
- 初始化列表:书写更简洁
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){}
析构函数
用于释放类的对象,C++规定带有“~类名”的成员函数就是析构函数
没有返回类型,没有参数,不能重载(只能有一个)
执行时机:与对象所在的内存区域有关
-
全局对象,位于全局数据区,会在程序结束执行时调用
-
局部对象,位于栈区,会在函数执行结束时调用
-
new创建的对象,位于堆区,通过delete删除时调用
友元
使得其他类的成员函数或者全局函数也能访问当前类的私有成员
友元的关系是单向的,且不可传递。
一般不建议定义友元类,只将某些成员函数定义为友元函数,更安全
下面的一个show为全局函数,一个为另一个类的成员函数
class Student;
class Teacher{
private:
int m_age;
public:
Teacher(int age):m_age(age){};
void show(Student* p);
};
class Student{
private:
char* m_name;
public:
Student(char* name):m_name(name){};
friend void Teacher::show(Student* p);
friend void show(Student* p);
};
void Teacher::show(Student* p){
cout <<"chengyuan"<< p->m_name << endl;
cout << m_age << endl;
}
void show(Student* p){
cout <<"quanju" << p->m_name << endl;
}
int main(){
Student s1("lihua");
Teacher t1(10);
t1.show(&s1);
show(&s1);
}
/* out
chengyuanlihua
10
quanjulihua
*/
类和struct的区别
- 默认权限不同
前者是私有,后者公共
- 继承不同
前者是私有继承,后者公共继承
- 模版
前者可以使用模版,后者没有
两者被区分开,有利于使得语义更加明确
string类
头文件
默认值空字符串
与C的字符串不同的是,string的结尾没有结束标志‘\0
拼接、增删改查
//string& insert (size_t pos, const string& str); 插入
s1 = "1234567890"
s2 = s1.insert(5, "aaa");
//out 12345aaa67890
//string& erase (size_t pos = 0, size_t len = npos); 删除
s3 = s1.erase(5, 3);
//out 1234590
//string substr (size_t pos = 0, size_t len = npos) const; 提取子字符串
s4 = s1.substr(3, 3);
//out 456
//size_t find (const string& str, size_t pos = 0) const;
//size_t find (const char* s, size_t pos = 0) const;
int index = s1.find("45", 0);
//out 3
find()是从第二个参数开始找,rfind()是找到第二个参数还没找到,则返回string::npos
find_first_of()是找到子字符串和字符串共同的字符在字符串中出现的位置
继承和派生
一个类从另一个类获取成员变量和成员函数的过程。继承和派生是同一个概念,但是站在不同角度,儿子接受父亲的称为继承,父亲给儿子的称为派生。
子类通过继承获得成员,还可以定义自己的新成员,以增强类的功能
减少代码量,更容易理解
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
class People{
private:
char* m_name;
int m_age;
public:
People(char* name, int age):m_name(name),m_age(age){};
People(){};
void SetName(char* name){m_name = name;};
void SetAge(int age){m_age = age;};
char* getName(){return m_name;};
int getAge(){return m_age;};
};
class Student: public People{
private:
int m_ID;
public:
void SetId(int id){m_ID = id;};
int getId(){return m_ID;};
};
int main(){
Student s1;
s1.SetName("sha");
s1.SetAge(13);
s1.SetId(12345);
cout << s1.getName() << endl;
cout << s1.getAge() << endl;
cout << s1.getId() << endl;
}
- 继承方式
继承方式中的 public、protected、private 是用来指明子类成员在父类中的最高访问权限的,高于最高访问权限的会被降级。
如果希望父类中的成员始终不能在子类的成员函数中使用,声明为private
如果希望父类的成员可以被子类毫无障碍的使用,声明为public或protected
如果希望父类的成员不能通过对象访问,还能在派生类中使用,只能声明为protected
在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。
修改访问权限:使用using关键字
class People{
private:
char* m_name;
int m_age;
public:
void show(){cout << m_name << endl;};
void setName(char* name){m_name = name;};
};
class Student: public People{
private:
//using People::show;
};
int main(){
Student s1;
s1.setName("lihua");
s1.show2();
return 0;
}
- 名字遮蔽
如果子类中的成员与父类中的成员重名,则不会构成重载,而是遮蔽
与函数的参数或者返回值无关
class People{
private:
char* m_name;
int m_age;
public:
void show(){cout << m_name << endl;};
void setName(char* name){m_name = name;};
};
class Student: public People{
private:
float m_score;
public:
void setScore(float score){m_score = score;};
void show(int x){cout << m_score << " iii " << x << endl; }
};
int main(){
Student s1;
s1.setName("lihua");
s1.setScore(90.0);
s1.show(5);
s1.People::show();
}
//out 90 iii 5
//lihua
- 构造函数
不能被继承,但可以在子类的构造函数中调用父类的构造函数
创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
- 析构函数
不能被继承
销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数
析构函数必须是虚函数,因为基类指针调用delete方法只能释放基类的内存,无法释放派生类特有的部分内存,进而导致内存泄漏。
class A{
public:
virtual ~A(){cout << "this is A" << endl;};
};
class B: public A{
public:
virtual ~B(){cout << "this is B" << endl;};
};
int main(){
A* a = new A();
a = new B();
delete a;
}
/* out
this is B
this is A
*/
虚基类
为了解决菱形继承中的二义性,虚继承的最终派生类只保留了一份虚基类的成员,所以该成员可以被直接访问
向上转型
将子类赋值给父类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TRtuzivv-1677900013083)(/Users/cx/Library/Application Support/typora-user-images/image-20230301160218302.png)]
多态与虚函数
分为静态多态和动态多态,静态多态主要是指函数重载或运算符重载,动态多态主要指子类重写父类中的函数。
作用:大大提高程序的复用性,提高代码的可扩充性和可维护性
class People{
private:
char* m_name;
int m_age;
public:
(virtual) void show(){cout << "wo shi ren" << m_name << endl;};
void setName(char* name){m_name = name;};
People(char* name, int age):m_name(name), m_age(age){};
char* getName(){return m_name;};
};
class Student: public People{
private:
float m_score;
public:
void setScore(float score){m_score = score;};
(virtual) void show(){cout << "wo shi xuesheng" << People::getName() << endl; }
Student(char* name, int age, float score):People(name, age),m_score(score){};
};
int main(){
People* s1 = new People("xiaohong", 15);
s1->show();
s1 = new Student("lihua",13,90.0);
s1->show();
}
/*out1
wo shi renxiaohong
wo shi renlihua
out2
wo shi renxiaohong
wo shi xueshenglihua
*/
通过基类指针只能访问派生类的成员变量和基类的成员,但是不能访问派生类的成员函数。
有了多态,只需要一个指针变量 p 就可以调用所有派生类的虚函数。
有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)
构成多态的条件:
-
必须存在继承关系
-
继承关系中必须有同名的虚函数,并且他们的函数原型相同,包括名字和参数和返回值等
-
存在基类的指针,通过该指针调用虚函数
除了指针,引用也可以实现多态,但是缺乏灵活性
虚函数表
虚函数表由编译器生成,如果一个类有虚函数,那么该类就会生成一个4个字节的虚函数表指针,指向虚函数表。指针存储在对象实例的最前面位置。
一个类的size不包含虚函数表,但是包含指向虚函数表的指针
虚函数表中的指针顺序,按照虚函数声明的顺序
多个基类之间的虚函数,按照继承的顺序,存放虚函数指针。
派生类重写的虚函数替换了基类虚函数指针,并指向了派生类的函数实现
七、运算符重载
一些经典的运算符重载:“+”可以实现字符串拼接,“<<”是位移运算符,同时也可以配合cout在控制台输出数据
规则
- 不是所有的运算符都能被重载,长度运算符
sizeof
、条件运算符: ?
、成员选择符.
和域解析运算符::
不能被重载 - 重载不能改变运算符的优先级和结合性,例如乘法的优先级永远高于加法
- 不能改变运算符的用法,例如“+”总是在两个操作数中间
- 运算符重载函数可以作为类的成员函数也可以作为全局函数,作为全局的时候可能需要在类内定义友元函数,因为大部分情况下会使用到类的私有成员。将运算符重载函数作为类的成员函数时,二元运算符的参数只有一个,因为有一个隐式的this指针。将运算符重载函数作为全局函数时,二元操作符就需要两个参数,一元操作符需要一个参数,而且其中必须有一个参数是对象,好让编译器区分这是程序员自定义的运算符,防止程序员修改用于内置类型的运算符的性质。
这是错误的!!!!因为没有对象
int operator + (int a,int b){
return (a-b);
}
- 箭头运算符
->
、下标运算符[ ]
、函数调用运算符( )
、赋值运算符=
只能以成员函数的形式重载。
那到底什么时候用成员函数形式呢?优先使用成员函数,这是因为初衷是增加类的扩展性,但他不能对称的处理数据,例如加法具有左结合性,不能对称的处理情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ujelx3l5-1677900013084)(/Users/cx/Library/Application Support/typora-user-images/image-20230302153929285.png)]
重载++和<<
#include<iostream>
#include <iomanip>
using namespace std;
class stopwatch{
private:
int m_min;
int m_sec;
public:
stopwatch():m_min(0),m_sec(0){};
stopwatch(int min, int sec):m_min(min),m_sec(sec){}
stopwatch run();
stopwatch operator ++(); //++i,前置形式
stopwatch operator ++(int); //i++,后置形式
friend ostream& operator << (ostream& , const stopwatch&);
};
stopwatch stopwatch::run(){
m_sec += 1;
if (m_sec == 60){
m_sec = 0;
m_min += 1;
}
return *this;
};
stopwatch stopwatch::operator++(){
return run();
};
stopwatch stopwatch::operator++(int x){
stopwatch s = *this;
run();
return s;
};
ostream& operator << (ostream& out, const stopwatch& A){
out << setfill('0') << setw(2) << A.m_min << ":" << setw(2) << A.m_sec;
return out;
}
int main(){
stopwatch s1(10,30), s2;
s2 = s1++;
cout << s1 << endl;
cout << s2 << endl;
s2 = ++s1;
cout << s1 << endl;
cout << s2 << endl;
}
/* out
10:31
10:30
10:32
10:32
*/
八、模版
函数模版
编译器可以根据传入的实参自动推断数据类型
函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板
template <typename T1, typename T2> T1 getThesametype(T1& a, T2& b){
T1 tmp = T1(b);
return tmp;
}
int main(){
int x = 5;
float y = 6.9;
cout << getThesametype(x, y) << endl;
}
//out 6
类模版
类模板和函数模板都是以 template 开头(当然也可以使用 class,目前来讲它们没有任何区别)
与函数模板不同的是,类模板在实例化时必须显式地指明数据类型,编译器不能根据给定的数据推演出数据类型。
template <class T1, class T2>
class A{
private:
T1 x;
T2 y;
public:
A(T1 _x, T2 _y):x(_x),y(_y){}
void print(){
cout << x << " " << y << endl;
}
void changex(T1);
};
template<class T1, class T2>
void A<T1, T2>::changex(T1 a){
x = a;
};
int main(){
A<int, float> p1(10,100.05); //如果写成A p1(10,100.05)就错误
p1.print();
p1.changex(100);
p1.print();
}
九、异常
程序错误
- 语法错误
编译和链接阶段就能发现,只有符合语法规则的代码才能生成可执行程序。这种错误最容易定位和发现
- 逻辑错误
代码思路有问题,可以通过调试来解决
- 运行时错误
指程序在运行期间发生的错误,例如除数为0,内存分配失败,数组越界,文件不存在等
运行时错误如果放任不管,系统就会执行默认的操作,终止程序运行,也就是我们常说的程序崩溃(Crash)。
C++ 提供了异常(Exception)机制,让我们能够捕获运行时错误,给程序一次“起死回生”的机会,或者至少告诉用户发生了什么再终止程序。
捕获异常
基本流程: 抛出(Throw)–> 检测(Try) --> 捕获(Catch)
检测到异常后程序的执行流会发生跳转,从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了
#include <exception>
using namespace std;
int main(){
string str = "http://c.biancheng.net";
try{
char ch1 = str[100];
cout << ch1 << endl;
}catch(exception e){
cout << "[1]out of bound!" << endl;
}
try{
char ch2 = str.at(100);
cout << 1 << endl;
}catch(exception &e){ //exception类位于<exception>头文件中
cout << "[2]out of bound!" << endl;
}
}
//out [2]out of bound!
发生异常后,程序的执行流会沿着函数的调用链往前回退,直到遇见 try 才停止。在这个回退过程中,调用链中剩下的代码(所有函数中未被执行的代码)都会被跳过,没有执行的机会了。
void func(){
throw "Unknown Exception"; //抛出异常
cout<<"[1]This statement will not be executed."<<endl;
}
int main(){
try{
func();
cout<<"[2]This statement will not be executed."<<endl;
}catch(const char* &e){
cout<<e<<endl;
}
}
//out Unknown Exception
异常类型
C++规定,异常类型可以是 int、char、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。
函数调用的形式参数和实际参数必须匹配,或者可以自动转换,否则就会在编译阶段报错
而catch只有在抛出异常时才会将异常类型和catch所能处理的类型进行匹配,这属于程序运行时
try{
// 可能抛出异常的语句
}catch(exceptionType variable){
// 处理异常的语句
}
如果不希望catch处理异常数据,可以只声明异常类型,不声明变量,例如下面这个例子
class Base{ };
class Derived: public Base{ };
int main(){
try{
throw Derived(); //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
cout<<"This statement will not be executed."<<endl;
}catch(int){
cout<<"Exception type: int"<<endl;
}catch(char *){
cout<<"Exception type: cahr *"<<endl;
}catch(Base){ //匹配成功(向上转型)
cout<<"Exception type: Base"<<endl;
}catch(Derived){
cout<<"Exception type: Derived"<<endl;
}
}
//out Exception type: Base
抛出异常
class OutOfRange{
private:
int m_type;
int m_len;
int m_num;
public:
void what();
OutOfRange():m_type(1){}
OutOfRange(int len, int num):m_len(len),m_num(num),m_type(2){}
};
void OutOfRange::what(){
if (m_type == 1){
cout << "error: unknown" << endl;
}
else{
cout << "error: the length is " << m_len << " but the index is " << m_num << endl;
}
}
class Student{
private:
string m_name;
int m_size;
public:
char operator[](int i) const;
Student(string name):m_name(name){
m_size = m_name.size();
}
Student(){}
string getName(){
return m_name;
}
int getSize(){
return m_size;
}
};
char Student::operator[](int index) const {
if( index<0 || index>=m_size ){ //判断是否越界
throw OutOfRange(m_size, index); //抛出异常(创建一个匿名对象)
}
return m_name[index];
}
int main(){
Student s1("xiaoming");
try{
cout << s1[10] << endl;
}catch(OutOfRange& e){
e.what();
}
try{
cout << s1[6] << endl;
}catch(OutOfRange& e){
e.what();
}
return 0;
}
/* out
error: the length is 8 but the index is 10
n
*/
exception类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uJWmhtTC-1677900013084)(/Users/cx/Library/Application Support/typora-user-images/image-20230303145527254.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2bfT5fgs-1677900013084)(/Users/cx/Library/Application Support/typora-user-images/image-20230303145604645.png)]
其他
重载、重写、重定义的区别
- 重载
函数重载或运算符重载,在同一访问区内,被声明的几个参数列表不同的同名函数,C++对函数名的修饰会把函数的参数类型加到函数名中,从而使得程序中函数名一样但在访问区中的函数名不一样。
- 重写
子类中重新定义父类中的除函数体外的其他都完全相同的虚函数,重写的一定是虚函数,访问权限可以自己定义
- 重定义
在派生类中,重新定义和父类名字相同的非虚函数,参数列表和返回值都可以不同
函数传递参数
形式参数:声明一个函数能够接受什么类型的实参
实际参数:传递给函数对应形参的具体内容
- 值传递:操作的对象是实参的拷贝
void change(int x1, int x2){
int tmp = x1;
x1 = x2;
x2 = tmp;
}
int main(){
int a = 1, b = 2;
change(a, b);
cout << a << b << endl;
}
//out 12
- 地址传递:又叫指针传递,通过指针传递实参地址
void change(int* p1, int* p2){
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
int main(){
int a = 1, b = 2;
change(&a, &b);
cout << a << b << endl;
}
//out 21
- 引用传递:C++特有的特性
void change(int& x1, int& x2){
int tmp = x1;
x1 = x2;
x2 = tmp;
}
int main(){
int a = 1, b = 2;
change(a, b);
cout << a << b << endl;
}
//out 21
编译过程
预处理(展开宏和内联函数)、编译(编译成汇编语言)、汇编(编译成二进制文件)、链接(链接其他所包含的库文件)
vector
- size()和capacity()
size():容器实际保存的元素个数
capacity():当前情况下容器允许存放的元素个数
- reserve()和resize()
reserve:指定容器的capacity,但不改变size
resize:在容器尾部添加(调用该元素对象的构造函数)或删除一些元素来达到指定的大小
- push_back()和emplace_back()
push_back():在容器尾部增加元素,vector大小改变但容量不变,如果大小超过了容量,就会自动扩容。增加的过程分为三步,构造一个临时对象,利用移动构造函数把临时对象的副本拷贝到容器末尾增加的元素中,调用析构函数释放临时对象。
emplace_back():调用构造函数直接在容器末尾增加一个元素,效率更高,但代码可读性降低,需要结合vector存放的元素类型判断增加的元素类型。例如v.emplace_back(100),100可能是指一个整数,也可能指100个vector容器
注:当对时间要求没那么高时,优先考虑push_back()