黑马C++核心编程小结
2.引用的注意事项
2.1引用必须初始化
int &a ;这个语句是违法的
2.2不要返回局部变量的引用
局部变量是建立在栈上的,局部函数执行完毕以后,内存就会被清理掉
如果函数做左值,那么必须返回引用
2.3函数列表中,为了防止形参改变实参,我们在引用加入const。
利用常量可以防止误操作修改实参
3.函数重载
3.1重载注意事项
当函数重载碰到默认参数的时候,在你输入一个值之后,编译器是不知道应不应该加默认参数的,这会造成歧义,需要避免
示例代码
void func2(int a, int b = 10)
{
cout << "func2(int a, int b = 10) 调用" << endl;
}
void func2(int a)
{
cout << "func2(int a) 调用" << endl;
}
int main
{
func2(10); //碰到默认参数产生歧义,需要避免
}
4.类和对象
4.1构造和析构函数
问题:两个函数内部运行时,地址,变量这些是如何分配的。
构造是为了给函数初始化,析构函数是为了对用完的对象进行清理
4.1.1 调用无参函数时,创建函数时不要给无参变量后面加(),否则会被编译器认为是一个证明。
例如
Person() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
age = a;
cout << "有参构造函数!" << endl;
}
void test02() {
//2.1 括号法,常用
Person p1(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
Person p2;
}
值传递的方式给函数参数,不太明白
//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
Person p; //无参构造函数
doWork(p);
}
/3. 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int *)&p1 << endl;
return p1;
}
void test03()
{
Person p = doWork2();
cout << (int *)&p << endl;
}
4.1.2 深拷贝与浅拷贝
如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区的问题。
4.2 拷贝函数调用时机
浅拷贝会造成的我们的一份数据在被调用完,被释放两次内存的问题,因此我们通过一个new关键字利用深拷贝在堆区创建一个新内存,会解决这个问题
这里内部的机理如何,在具体项目如何调用
4.2.1静态成员变量
静态成员函数和变量都是存储在堆区的,不占栈区的内存。
类中的静态成员函数只能访问静态成员变量,同时静态成员也是有权限的,放到私有权限我们照样也是访问不到的。
4.3 对象模型和this指针
4.3.1 this指针
类中自定义函数的所有对象公用一段代码,函数如何确定什么是哪个对象调用自己的?C++提供了指针解决这个问题。
指针的两个用途:
形参和成员变量同名时,可以用this指针进行区分
类中的非静态成员函数需要返回对象自身时,可以使用return *this
示例:this->age = age; 这句代码中中,this指向的变量表示成员函数。
4.3.2空指针
如果成员函数中没有调用this指针的话,空指针也是可以调用成员函数的。如果用到this指针的话,需要加一个if条件代码,增强代码的健壮性。
4.3.3常含数
常含数:成员函数后加const后,就成为了常含数,里面的成员变量就是不能更改的。如果必要情况下需要修改,只需加入关键字mutable.
常量对象:对象名前加const表示常量对象,如果一个对象成为了常量对象我们就不能再修改了,但是是可以访问的。
4.4友元
前面我们说过,类中的私有权限,其他的函数无权访问,但是对于某些特殊情况下,一些函数需要私有权限信息。怎么办?C++定义了一个叫做友元的关键字,在函数前加入关键字friend。这样就可以访问了,这个关键字是不是非常形象?
友元分为:全局函数,类,成员函数做友元
==问题:private:
Building *bu;==第二句代码什么意思?
4.5运算符重载
运算符重载是为了解决,我们在类中对多个属性值进行运算符时,编译器不知道怎么计算的问题,所以进行重构。
比如,m1里面有MA和MB两个属性值;m2里面有两个属性值,如果直接m1+m2,我们想的是m1和m2的两个属性值分别相加,但是编译器知道要这样运行吗?所以要进行类中属性的运算符的重载。重载函数是固定的形式,如果加就是operate+,如果减就是operate-。等等
4.6继承
4.6.1基础概念
继承的目的是为了让子类能够继承一些父类中的共有属性,这样就不用再重写重复的属性了,提高代码的复用性。
语法:class Java : public BasePage JAVA 是子类(派生类),BasePage是父类(基类)
示例:
class Java : public BasePage
{
public:
void content()
{
cout << "JAVA学科视频" << endl;
}
};
4.6.2继承方式
保护,公共,私有继承
公共继承的子类可以访问父类公共属性,和保护属性 不可访问私有属性
其他类通过这个子类间接访问父类的属性时,只能访问原父类中的公共属性。
保护继承的子类可以访问父类的公共属性和保护属性,不可以访问私有属性。
其他类通过这个子类间接访问父类,只能访问到原父类中的保护属性,其他的属性都不可访问。
私有继承的子类可以访问公共属性和保护属性,不可访问私有。
其他类间接访问父类属性时,什么属性都访问不到,因为子类是私有继承。
总结以上:三种继承方式主要是限制其他函数或类访问继承了父类的子类的权限,重点是其他函数。
4.6.2继承中构造与析构顺序
我们在函数中调用子类,子类要先继承父类,所以构造函数是先构造父类,有了子类才能构造子类。
析构时,先把这个子类析构掉,析构完以后父类函数用不到了再析构,个人的理解这两个过程就好像穿衣服时要先传内衣,再穿外衣;脱衣服时要先脱外衣,再脱内衣。
4.6.3继承碰到同名时
当子类和父类碰到同名的成员函数或者同名变量时,为了防止搞混,子类函数会将父类函数隐藏掉,如果想访问父类同名的函数,只需要在前面加入父类作用域即可 Base::,碰到静态成员变量重名的情况是一样的。
4.6.7 多继承语法
C++允许一个子类继承多个父类,这种继承方式我们称之为多继承
这种方式一般我们不提倡使用,因为我们不知道多个父类里面的变量会不会有重名的,如果有重名的变量;可能会导致意外的错误。
4.6.8菱形继承
父亲生了两个孩子,两个孩子生了一个孙子,这种方式称之为零星
4.7 多态
首先要明白C++的语言的三大特性:封装,继承,多态。
多态分类:
静态多态:函数重载和运算符重载属于静态多态。
动态多态:派生类和虚函数实现运行时多态。
静态多态和动态多态时的区别:
静态多态的函数地址早绑定-编译阶段就确定了地址;
动态多态的函数地址-运行阶段时才确定函数地址。
4.7.1 多态的引入
我们以代码为例进行讲解
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal)
{
animal.speak();
}
//
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}
int main() {
test01();
system("pause");
return 0;
}
在这段代码中,我们定义了父类和两个子类的speak函数,目的是想实现创建猫的实例就运行猫的speak函数,创建狗就走狗的speak函数,但是我们发现写出代码之后,所有的实例都走的是父类animal的speak函数。为了实现一种,每个子类能个性化的实现和父类中同名的函数功能,C++定义了一个关键字virtual,跟在这个函数后面的函数称之为虚函数,所谓的“虚”,在编译上的原理就是父类函数没有那么“霸道”了,不会再出现,定义猫还是出现animal的情况了,这恰好就满足了我们的需求。
由上也可以看到,满足虚函数的条件:
子类重写父类的函数;
父类的指针或引用指向子类对象。具体看这几行代码
void DoSpeak(Animal & animal)
Cat cat;
DoSpeak(cat);
4.7.2 有关于多态的实例——实现计算器功能
//普通实现
class Calculator {
public:
int getResult(string oper)
{
if (oper == "+") {
return m_Num1 + m_Num2;
}
else if (oper == "-") {
return m_Num1 - m_Num2;
}
else if (oper == "*") {
return m_Num1 * m_Num2;
}
//如果要提供新的运算,需要修改源码
}
public:
int m_Num1;
int m_Num2;
};
void test01()
{
//普通实现测试
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;
cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}
//多态实现
//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator
{
public :
virtual int getResult()
{
return 0;
}
int m_Num1;
int m_Num2;
};
//加法计算器
class AddCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};
//减法计算器
class SubCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 - m_Num2;
}
};
//乘法计算器
class MulCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 * m_Num2;
}
};
void test02()
{
//创建加法计算器
AbstractCalculator *abc = new AddCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc; //用完了记得销毁
//创建减法计算器
abc = new SubCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
//创建乘法计算器
abc = new MulCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;
cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
可以看出来,利用多态的设计架构,能够使得代码结构更加清晰。多态的思想是被提倡的。
问题:AbstractCalculator *abc = new AddCalculator; 这行代码不是太理解
4.7.3纯虚函数
通过上一节的分析,我们也看到了,父类的虚函数一般都是没什么用的,主要都是要用子类重写它,所以不如只写一个虚函数,让它做一个空实现。这种特殊的虚函数我们称之为纯虚函数,也叫做抽象类。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
抽象类的特点:
无法实例化对象,因此base = new Base;是非法语句
子类必须重写抽象类中的纯虚函数,否则也是抽象类。
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func()
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
多态案例2:制作饮料
代码:
class AbstractDrinking {
public:
//烧水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void MakeDrink() {
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking {
public:
//烧水
virtual void Boil() {
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething() {
cout << "加入牛奶!" << endl;
}
};
//业务函数
void DoWork(AbstractDrinking* drink) {
drink->MakeDrink();
delete drink;
}
void test01() {
DoWork(new Coffee);
cout << "--------------" << endl;
//DoWork(new Tea);
}
4.7.4 虚析构函数和纯虚析构函数
在使用多态的时候,如果子类有数据通过new的方式开辟到堆区,那么在父类指针指针进行释放的时候是无法调用子类的析构代码的。可能会造成内存泄漏的问题,解决这种问题的方式就是让父类变成虚析构或者纯虚析构函数。