自己上课的笔记
基础知识
区域
输入输出
输入输出只能对应基本类型,自己定义的不行
但可以用输入输出流的重载来输入输出(之后讲)
stdin 键盘
stdout 屏幕
键盘和屏幕的类型是FILE*
stderr屏幕标准错误数据
初始化
花括号初始化一切变量,但不能定义已经定义过的变量
const与指针
指针
此时p=&a,*p=a。b=10是因为obj文件的时候a=10.
预编译的时候替换#define,编译的时候替换常变量
const
const和指针结合的时候可以为空,但是和引用结合的时候必须初始化
权限
判断
形参实参的传递
引用
C++提供了两种引用:左值引用和右值引用
定义
定义引用时必须给予初始化,没有空引用和引用的引用
左值右值的引用
常引用
函数参数
普通交换函数:
加引用之后的交换函数:
函数调用
数组的引用
引用与指针
引用是指针的语法糖
const 指针与引用
缺省参数
普通缺省函数
随机值做默认值的缺省函数
第二次调用的时候b是随机值
函数的重载
函数的重载称为编译时的多肽
什么是多态:多态分为两种,一种是编译时多态,一种是运行时多态,编译时多态就是函数名相同但参数表不同,在编译时根据实参匹配相应的函数。
函数重载:由名字粉碎(把返回类型和参数类型作为名字的一部分)形成的obj目标文件的函数名来区分,不能用返回类型来区分
函数二义性
函数模板
名字空间
new
new和malloc
区别
new在底层仍然调用的malloc,new和malloc申请的都 是堆空间。
在一般情况下,栈的申请速度比堆快。
new申请空间
new函数调用
定位new
delete new
C++11的新特性(部分)
aotu
类型引导
aotu推导规则
auto定义的变量,可以根据初始化的值(即变量必须初始化),在编译时推导出变量名的类型
aotu作为形参
aotu的限制
认识静态和非静态
非静态成员不可以使用aotu
总结
aotu使用
decltype
基于范围的for循环(范围for)
指针空值nullptr
typedef与using
string与string.h
C++面向对象编程
面试C++分为四个块,深入理解C++面向对象的编程思想、熟练掌握继承封装多态、熟练使用STL库、C++11
图纸就是类
类是设计的产物,来自于抽象概念世界,概念是对实体世界分析的产物,对象是类型实例化后的具体表现形式。
状态和行为是对象的主要属性
对象的关系
C++三大特性 封装 继承 多态
封装是将属性和方法拿类型作为一个整体来进行,属性设计成私有,方法设计成公有,这样创建对象的时候不能直接通过对象修改对象的属性,而是要通过方法来修改,方法是调用的时候对传入的实参值有安全性检查,这样才可以体现封装的特性。
继承允许一个类继承另一个类的属性和方法。通过继承,子类可以复用父类的代码,同时还可以添加自己的新特性或修改父类的行为,从而实现代码的重用和扩展。
类型设计与实例化对象
成员函数
数组指针
举例
内联函数
this指针
this指针作用
计算机有内存资源和空间资源,引入this指针的根本目的就是节省资源。
举例
this指针特点
构造函数与析构函数
构造函数:1.创建对象 2.初始化对象 3.类型转换
构造函数的定义与使用
只要我们定义了一个构造函数,系统就不会自动生成缺省构造函数,只要构造函数是无参或各参数均有缺省值的,C++编译器都认为是缺省的构造函数,并且缺省构造函数只能有一个
重载调用
初始化对象
如果要用到圆括号里面一定要有实参,如果没有实参最好不要用圆括号,因为系统会认为是函数声明,比如把Int c(50)改成Int c(),此时会把c认为是函数类型。
构造函数中使用this指针
构造函数的生存周期
析构函数定义
创建对象
const和this指针在构造函数
const和this指针
程序的通用性
全局变量的可见性
赋值
多参无法类型转换
多参的使用
(char)a;这才是类型转换
如果构造函数作为强制类型转换,那么构造函数要是单参(可以给第二个参数赋默认值)
a=(Int)(x,y);单参才能做类型转换,所以此处只能取一个值,因为(x,y)是逗号表达式,所以取后面的数即y,所以是20.
explicit关键字与将亡值
复数
函数引用与指针
1.
使用 int& func() 声明,返回的是一个变量的引用,引用是对象的别名,在 func 函数中,强调返回的是某个对象本身,返回的引用和原对象是同一个实体,对引用的修改会直接影响原对象。引用一旦初始化就不能再引用其他对象。由于引用不能为 nullptr,在使用引用时不需要额外检查其有效性,但如果返回的是悬空引用,后果可能比较隐蔽。
使用 int* func() 声明,返回的是一个指向 int 类型的指针,返回的是对象的内存地址,指针可以在不同时刻指向不同的对象,并且可以为 nullptr,表示不指向任何对象。指针可以为 nullptr,在使用指针之前通常需要检查其是否为 nullptr,避免对空指针进行解引用操作。不过返回悬空指针时,检查 nullptr 并不能避免未定义行为。
2.
左边 func 函数的参数 c 是一个 int 类型的引用。引用是对象的别名,它直接绑定到传递给函数的实参上,在函数内部对 c 的操作实际上就是对实参的操作。右边的调用 func 函数时,需要传递变量的地址。可以使用取地址运算符 & 获取变量 a 的地址,也可以直接传递已经存储了变量地址的指针 ra
可以看出处理器处理别名的时候以指针来看待。
多函数赋值
调用外部函数
如果不是用new和delete而是用malloc和free,那么就不会多出析构函数那四个字节的空间,这只是new和delete的协议规定
拷贝构造函数
拷贝构造函数的认识
拷贝构造函数函数名与类名相同
拷贝构造函数的用途:1.拿一个对象初始化另一个对象 2.形参是值类型时,实参和形参的结合是拷贝构造 3.返回值类型,构建亡值对象,调用拷贝构造
定义拷贝构造函数
value(x.value) 会将新创建对象的 value 成员初始化为传入对象 x 的 value 成员的值。这是拷贝构造函数的核心功能,即创建一个新对象,使其成员变量的值与传入对象的成员变量的值相同。
拷贝构造函数的使用
此函数Getint用引用返回,此时就可以返回此对象的地址(或说是用引用返回)
这一段函数在主函数中构建了一个tmp的副本(将亡值),值为100,此时Getint完成,然后释放tmp和x,当将亡值赋值给a之后也被析构掉。
浅拷贝
1.浅拷贝问题:
如果 MyString 类中包含一个指向动态分配内存的指针(例如 char* str),那么直接赋值 this->str = s.str 会导致两个对象共享同一块内存。这可能会导致双重释放(double free)错误,因为当其中一个对象析构时,
它会尝试释放这块内存,而另一个对象仍然在使用它。
2.内存泄漏:
由于浅拷贝,原始对象中的内存不会被正确释放,从而导致内存泄漏。
3.未定义行为:
在析构函数中,如果两个对象共享同一块内存,当其中一个对象被销毁时,另一对象的析构函数可能会尝试释放已经被释放的内存,导致未定义行为。
此时s3和s1都指向yyy字符串的首地址,delete s3的时候已经把yyy释放了,此时s1的指针所指内容也空了,再把s3指针置空,但s1指针仍指向这块空间地址。释放s1的时候这块空间再次被释放,出现错误。
深拷贝
1.先判断s1是否是给自己赋值 2.释放s3空间 3.计算s1的str长度 4.申请新空间长度len+1 5.拷贝
运算符的重载
禁止重载的运算符:
加法:对象和对象、对象和变量
加法:变量和对象
赋值运算符
return *this而不是return x的原因是遵循了连续赋值c给b,b给a的规则,如果返回x,就是c给b,c给a。
生存期不受函数的影响才可以以引用返回,否则要按值返回。
前置加加和后置加加
强制类型转换
输入输出流
类型与值类别
可以取地址的标识名就是左值,不可以取地址的是右值,字面常量是纯右值,纯右值是不可以被改变的比如10,12.23等。将亡值如果具名就是左值,如果不具名就是右值。
总结:所有具名变量或对象都是左值,而右值不具名。
eg:int a=10;//a是一个左值,10是右值
const double dx=12.23;//dx是左值,12.23是右值
#define MAX 10 //MAX 是一个宏,它在预处理阶段进行文本替换,不具有左值或右值的属性。
int a=10;
&a++;
错误,后置加加是先把a放到临时量里面,此处相当于是给临时量取地址
&++a;
正确,先把a取出来加加再放回去取地址。
类类型
类类型是一种用户自定义的数据类型,它将数据(成员变量)和操作这些数据的函数(成员函数)封装在一起
引用举例
引用型别未定义
完美转发(在型别未定义的时候保证t的型别不变)
移动语义
移动语义深入了解
如果是s2=s1,则为深赋值,此时是先把s2的空间释放,计算s1空间大小,开辟新空间分配给s2,最后拷贝。
MyString s3(std::move(s1));是s3的str指向s1的str所指内容,但是s1的指针被置空,浅拷贝就是s1指针不被置空仍指向这块空间。
移动构造和移动赋值
类型有堆资源的时候要使用移动语义,没有使用堆资源则不使用,有内核资源的时候也要使用移动语义。
移动语义赋值
这段代码中 s1 = func(“yhpinghello”); 运用了移动语义的赋值。func 函数返回的是一个临时对象(右值),在进行赋值操作时,会调用 MyString 类的移动赋值运算符 MyString& operator=(MyString&& s),将临时对象的资源(即 str 指针)转移到 s1 中,避免了不必要的内存复制.
利用移动语义实现加法赋值
采用引用计数的方式管理字符串的内存
在整个运行过程中,s1、s2 和 s3 三个对象共享同一块内存,通过引用计数机制管理内存的释放。只有当所有引用该内存的对象都被销毁,引用计数变为 0 时,才会释放这块内存,避免了不必要的内存复制和重复释放。同时,代码中定义的移动构造函数 MyString(MyString&& s) 在这个 main 函数的执行过程中并未被调用,因为没有涉及右值对象的传递。
封装和管理 IPv4 网络地址信息
头文件
inrtaddress:
connector头文件:
MyStringAppend的实现
左值赋值
测试:
移动语义
给尾部添加字符
测试:
给尾部添加字符串
从第pos个下标开始插入count个字符
把s的count个字符给s1
给尾部添加字符串
三个重载的 operator+= 函数实现字符串的追加操作
主函数
友元函数
外部函数友元
成员函数友元
类友元
总结
如果test是base的友元,那么tast可以访问base的私有属性,但base不可以访问test,所以友元是单向的,不具有自反性、传递性和继承性。友元打破了封装特性,且友元是自成一体的不受制于公有私有或保护类性质。
static
如果定义了一个全局变量pi=3.14159又定义了一个类的静态成员pi=3.14,此时打印两个pi:
属性静态
通过设计一个圆类型来举例
pi值是共有成员,可以把pi值变为全局量:
此时不可以再用Circle() :radius_(0), area_(0),pi(3.14)这种方法,这属于再次构建pi,因为pi在类中声明了是静态成员变量,在进入到主函数之前就应该完成初始化,且整个程序运行期间只能被创建一次,所以要添加一个类外的初始化。
如果已经定义好pi是静态成员函数且已经在类外完成初始化,那么在类里面的函数中加上const之后再写入pi+=3;也是可以允许运行的,因为静态成员函数无this指针的约束(如果是this->area+=10;是不允许通过的):
定义整型类型静态常性成员变量的时候在static后加上const就可以在类里面直接初始化(数组、指针、double等不可以直接在类里初始化):
1.信息共享
全局变量:
2.静态数据成员属于整个类型
3.在类的成员函数中使用静态数据成员
静态方法
C++ 中静态方法不能定义成常方法
在 C++ 里,静态成员函数属于类本身,而不属于类的某个对象,它不与任何特定的对象实例相关联,没有隐含的 this 指针。
常成员函数的本质是修饰 this 指针,表明该函数不会修改调用它的对象的成员变量。由于静态成员函数没有 this 指针,所以不能将其定义为常成员函数。
但是如果静态成员是私有的那么在类外的函数中就算前面加上类域也不可以编译通过。
修改一下print()函数达到可以调用sum的目的:
静态方法有两种调用,一种是t2.print(),一种是Test::print(t2)
非静态方法只可以用2…func()这种方法来调用
模板
模板是生成代码的代码
推演的时候生成了两个函数,一个生成整型,一个生成double。
时间戳类型的实现
在代码添加头文件#include<stdint.h>就可以使用uint8…64_t了
头文件
源文件
测试代码
运行结果
testTimestamp:
循环一万次需要多少ms
继承
继承是向上兼容,可以将父类指向子类的地址。
面向对象的三大特征
封装:在类型设计时,把类型和方法形成一个整体并将属性和一些方法(工具方法)设计成保护和私有。
继承:拿一个类型继承另一个类型
多态性:分为运行时多态和编译时多态,编译时多态包括运算符和函数的重载,运行时多态是程序执行过程中运行的多态。
继承的概念与定义
拿class设计派生类和拿struct设计派生类所默认继承的方式不一样(未给出访问限定符),class是私有性继承,struct是共有性继承
编制派生类
基类中的构造函数和析构函数无法继承
访问权限
此处ob=2是被允许的,因为fun是Base的成员函数,base无法直接访问ob,但fun可以访问ob,所以此处base调用fun是可以访问ob的
同名隐藏
这两张图的func函数里面的num都是Base的num,类域中子类域优先,这就叫同名隐藏。
此时base.func中的func是Base的func,这也叫同名隐藏,系统已经把基类的func隐藏掉了,不过定义base的时候调用的是Object的构造函数。并且即使给派生类的func加一个参数,两个func都不叫函数重载,因为函数重载一定要在同一个作用域里面。
命名冲突
编译器可分清这几个value,粗体的是成员,浅色的是形参。
先使用形参value,最好给出this指针:
继承的兼容性
赋值兼容性只适用于公有继承
切片现象
派生类"学生"给基类"人"赋值的时候是只把人赋值,其他的切掉不赋值,这就是切片现象。
赋值兼容性规则
基类不能给派生类赋值
父对象的指针可以指向子对象的地址,也可以父对象的引用引用子对象
此时打印出来的是随机值,因为子类空间大于父类,强转后多余的空间将会打印随机值。
私有继承
构造与析构函数
先构建基对象再构建派生对象,析构的话则是先子后父。
继承与静态成员
#include
using namespace std;
class Object
{
protected:
static int num;
private:
int val;
public:
Object(int x = 0) :val(x) { }
~Object() {}
};
int Object::num = 0;
class Base : public Object
{
private:
int sum;
public:
Base(int x = 0) :Object(x), sum(x)
{
cout << "Create Base : " << ++num << endl;
}
~Base() { cout << "Destroy Base : " << num-- << endl; }
};
class Test :public Object
{
private:
int id;
public:
Test(int x = 0) :Object(x), id(x)
{
cout << "Create Test : " << ++num << endl;
}
~Test() { cout << "Destroy Test: " << num-- << endl; }
};
int main()
{
Test t1, t2, t3;
Base base1, base2;
}
静态成员num只能在类外初始化,域是类域,
但是我们期望 Test 和 Base 类各自拥有独立的 num 计数器,然而当前代码里 num 是 Object 类的静态成员,这就致使所有继承自 Object 的类都共享同一个 num 变量。若要达成 Test 和 Base 类各自拥有独立的 num 计数器,可分别在 Test 和 Base 类里定义静态成员变量,代码如下:
继承与友元
友元函数不具有传递、逆反、继承特性,eg和基类是友元但不一定跟派生类也是友元
多重继承与派生类成员表示
面试大概率要问,但菱形继承在设计的时候尽量避免
但要注意一个基类不能被在同一个派生类连续中多次继承,如:
菱形继承
菱形继承的时候基对象会被创建成两份,有数据的冗余,继承的时候可以通过虚继承来解决。
数据冗余和二义性
解决二义性的方法:
多态性与虚函数
编译时多态
运行时多态
运行时多态=公有继承+虚函数+(指针或引用调用虚函数)
#include
#include
#include<assert.h>
using namespace std;
class Animal
{
private:
std::string a_name;
public:
Animal(const std::string& name) :a_name(name) {}
virtual void eat() const {}//虚函数
};
class Dog : public Animal
{
private:
std::string owner;
public:
Dog(const std::string& own, const std::string name)
:Animal(name), owner(own){}
virtual void eat() const { cout << "Dog Eat: bone " << endl; }
};
class Cat : public Animal
{
private:
std::string owner;
public:
Cat(const std::string& own, const std::string name)
:Animal(name), owner(own){}
virtual void eat() const { cout << “Cat Eat: fish” << endl; }
};
void funa(Animal a)
{
a.eat();
}
void funb(Animal* p)
{
assert(p != nullptr);
p->eat();
}
void func(Animal& s)
{
s.eat();
}
int main()
{
Dog dog(“yhping”, “hashqi”);
Cat cat(“tulun”, “xiaogou”);
funa(dog);
funb(&dog);
func(dog);
funa(cat);
funb(&cat);
func(cat);
return 0;
}
运行结果:
我们发现第一个对象调用的是基类构造函数,引用和指针调用的话调用的是派生类构造函数。
虚表和虚表指针***
编译器编译的时候先形成一个Object的虚表(注:RTII(运行类的识别),00 00(分隔符)),接下来编译Base的时候会把Object的虚表拷贝下来变成Base的虚表,此时Object的RTII变成Base的RTII,add、fun、show都重写了,那么就在Base里面把新写的这三个函数覆盖之前的三个函数,同理,Base的虚表拷贝下来变成Test的虚表,此时Base的RTII变成了Test的RTII,且重新的函数把Base拷贝下来的函数也给覆盖掉。基类和派生类的RTII元数据根据链式结构来确定基类和派生类。如果定义了Test的对象,那么也会先调用基类的构造函数,此时最先调用的是Object的构造函数,构造函数要填虚表,此时该虚表指向Object,当构建完成Object后,我们再来构建Base,Base将重置虚表,指向Base的首地址,构建完Base之后再来构建Test,此时虚表指向Test,那么虚表指针指向谁的首地址就要看谁是最终被调用的构造函数,所以addd调用的是Test的,fun调用的是Base的,print是Test的。
所以只有一份虚表,存放的是虚函数的地址,调用函数的时候要看是否被重写被覆盖,从而判断出调用的是哪个构造函数。
但是op是Object的指针,不能指向show。
定义虚函数的规则
如果虚函数virtual和内联inline同时出现,那么系统取舍,只取多态性virtual。
此时第一个op调用的是派生类的func方法,第二个op指向的是基类的func方法。
此时在基类Object添加一个虚函数add(一个参数),在派生类Base也写一个add的虚函数(两个参数),此时op->add(12,23)无法编译通过,因为op是Object的指针,无法到达Base的add。
如果想调用Base的add,那么可以强转:
但是这样是危险的,因为这样成立的前提是该指针指向了派生类对象,如果是这样:
系统就会崩溃,因为查虚表的时候查的仍然是Object,没有add(双参),基类的指针可以强转成派生类指针,但是不安全,基类指针指向派生类对象的话没问题,但是指向基类对象强转就会出现问题,所以轻易不要使用强转。
解释第1条
重写虚函数要返回类型相同,函数名相同,参数表相同,如果返回类型不同将无法编译通过,因为无法拿返回类型来确定调用规则,只能通过参数表来确定调用规则,无法靠返回类型来区分。
问题回答
多态的原理
静态联编和动态联编
静态联编是早期绑定,编译时将函数名和函数所在的地址替换掉,动态联编是在运行时才能找到函数地址,通过虚表找到函数地址,寄存器来辅助获取虚表指针。
题目解答
错误:
第 8 行memset(this,0,sizeof(Base)); 存在两处问题:一是Base未定义,编译器会报错;二是即使将Base改为Object,memset将对象的内存全部清零,会破坏虚函数表指针,导致后续虚函数调用出错,同时也会清除已初始化的成员变量value的值。
第 17 行op->add(2); 中op没有定义。
问题:在Object类的print函数中调用add(1),当通过Base类对象调用show函数进而调用print函数时,虽然add是虚函数,但由于在Object类的成员函数内调用,可能不符合多态调用的预期(因为此时的调用环境是Object类的函数内部)。
错误:构造函数和析构函数中调用虚函数add存在风险,在构造和析构期间多态机制可能未完全建立或已部分销毁,此时调用虚函数可能达不到预期多态效果,甚至引发错误 。
问题:op 调用的是 Base 类中重写的 func 函数,但使用的默认参数是 Object 类中 func 函数所定义的。输出结果是:Base::func: b 10。这是因为默认参数是在编译时确定的,而不是运行时。编译器会根据指针的静态类型(即 Object*)来确定默认参数的值。在 Object 类中,func 函数的默认参数 a 被设置为 10,所以当调用 op->func() 时,即便实际调用的是 Base 类的 func 函数,使用的默认参数仍然是 Object 类中定义的 10,而不是 Base 类中定义的 20。
所以静态联编确认了可访问属性和默认值,但调用时是动态联编,无所谓访问属性。
虚析构函数
如果有虚函数的话那么最好把析构函数定义为虚函数,如果没有的话就最好不要把虚构函数定义为虚函数。
纯虚函数和抽象类
抽象类存在的意义主要是作为基类,为派生类提供统一的接口规范,派生类必须重写抽象类中的纯虚函数,才能被实例化。
final和override关键字
动态多态和静态多态
面试题
1.你是如何理解重载、覆盖(重写)、隐藏 (重定义)。
2.虚表
3.虚析构函数的作用。
4.静态绑定?动态绑定?(详细说)
5.C++ 的 4 种类型转换?应用场景?
6.RTTI(Runtime Type Identification)是 “运行时类型识别”
7.多重继承与虚函数
四个类型转换
静态转换
static_cast
适用于基本数据类型之间的转换,比如char short int long int ;long long;double float long double;bool等,还有一个特别的就是可以转换枚举类型,但是不能用于数组转换。
静态:编译的时候识别。
枚举和枚举类的区别
如果想作为枚举常量那只能在一个枚举里面做,所以下面这个代码会报错:
但枚举类可以通过,可以使枚举常量重复。
语法区别:
枚举类型的转换
此处a使用静态转换把枚举变量转换成整型值。
可以和void*关联
静态转换唯一对静态有效的就是无类型指针
cvp是常性指针,如果要转换要变成ip = static_cast<const int*>(cvp);但是ip不是常性指针,所以要保持一致:const int ip = static_cast<const int>(cvp);
打印*cp的值是d,因为在16进制里61是a,62是b,64是d,在下面把a的首地址给了p,然后又将p赋值给cp,在打印的时候解引用就是解一个字节,由于是小端存放所以解的是低地址,64存放在小端(高位数存放在高地址,地位数存放在低地址),解下来就是64。
类的类型转换
第二个向下转换也是极其不安全的。
常性转换
const_cast
其实也就是去常性,但是对内置类型也可以说没用,因为如果要改变但输出结果仍不是自己想要的,但是对自己设计的类型有作用。
1.
运行结果:
因为内置类型的常量在编译时属于替换原则,输出的时候a还是10,但ra+=100也确实改变了a的值。
此时打印出的就是100和200,因为a是自己设计的类型。
重新解释转换
reinterpret_cast,主要针对指针
重新解释相当于c语言的类型强转。
此时打印出来的仍是10,因为我们自己设计的也是四字节,这四个字节存放的是a中的value值,只不过系统把Int识别成int,我们把指针指向加等100也就是把value值加等100,此时value是110,我们对此地址进行重新解释。
如果类加入一个char类型的ch:
此时a对象是8字节(对齐),上面四字节有一个字节存放的ch,下面四字节存放的是value,把首地址给指针。
打印ch的时候只打印d,如果把ch强转成int类型,那么打印的就是一个int,即abcd:
动态转换
dynamic_cast,与RTTI有关
只有类型有虚函数才具有动态转换性,没有虚函数就没有动态转换性。
虚表存放的是虚函数的地址和元数据。
动态:运行过程中要查虚表,通过虚表判断转换的合法性,在运行的时候来识别。
op给bp可以,因为op本来就指向bp,但op给tp不行,所以我们可以用到动态转换:
此时tp是空指针。
例题
没虚函数就没有虚表,没有虚表就没有RTTI
json和protobuf中protobuf可以压缩。