Lever_2_ C++基础 Day 8-Day 15
Day_8
1- 面向过程加工的是: 一个一个函数;
面向对象加工的是:一个一个的类。
2- 类的调用
类的调用 执行过程分析 ==> 类代码不是一步一步的指向过程。
类是一个数据类型(固定大小内存块的别名); 定义一个类,是一个抽象的概念,不会立即给你分配内存。用数据类型定义变量的时候,才会分配内存。
3- 定义命名空间
//这样就可以避免多个工程(文件)使用了相同的变量名、函数名的问题。
namespace nameA
{
int a=10;
}
namespace nameB
{
int a=20;
}
//在使用的时候,在 main中记得先声明再使用:
using namespace nameA;
using namespace nameB;
cout << nameA::a << endl;
cout << nameB::a << endl;
//甚至可以在命名空空间里面再嵌套一个命名空间。(使用的时候只用声明最外面的)
4- 三目运算符 (a < b ? a : b )
在C语言中, 表达式的返回值 是变量的值;
在C++中, 表达式返回的是变量的本身
int a=10,b=20;
(a<b?a:b)=30;
//在C++中,就是把30赋值给a; 在C中就是错的,不能10=30;
//在C中,三目运算符一般写在等号右边。
*(a<b?&a:&b)=30; //这样C语言就可以把三目运算符写在左边了
5- const
const 的对象的内存在符号表的位置。(不同于堆栈)
如果关键字 const 出现在星号左边,表示被指物是常量;(只能使用,不能修改)
如果关键字 const 出现在星号右边,表示指针自身是常量;
const 在两边都出现,那都不能修改;
//const 修饰的是 指针所指向的内存空间,不能被修改
int operatorTeacher01(const Teacher *pT)
{
//pT->age = 10; //报错
return 0;
}
//const修饰的是指针 指针变量本身不能被修改
int operatorTeacher02( Teacher * const pT)
{
pT->age = 10;
//pT = NULL; //报错
return 0;
}
//指针和指针变量都不能被改变
int operatorTeacher03( const Teacher * const pT)
{
//pT->age = 10;
//pT = NULL; //
printf("age:%d", pT->age); //但是可以读
return 0;
}
const**在C语言中没啥用**,可以由指针间接赋值,给绕过去。
6- C++ 中 const 和 define
当const常量为全局,并且需要在其它文件中使用,会分配存储空间;
而 define 只起一个替换作用。
7- 引用 Type &name = var
引用可以看作一个已定义变量的别名,是C++的独特语法。
int a=20;
int &b = a;
相当于a b 现在共用一个地址,引用过后一变都变啊!
引用作为其它变量的别名而存在,因此在一些场合可以代替指针。相对于指针来说具有更好的可读性和实用性。
引用在C++中的内部实现是一个常量指针
Day_9
1-引用
指针的引用:
T* &p;
类比: int * &a; //只不过这里是结构体 T*
常引用:让变量拥有: “只读”属性
-
用变量 初始化 常引用
int x=20; const int &y = x; //常引用 让变量 引用只读属性 不能通过y去修改x了
-
用字面量 初始化 常量引用
const int a = 40; //c++编译器把a放在 符号表 中 错:int &m = 41; //报错!常量没有内存地址 //引用 就是给内存取多个门牌号 (多个别名) 对:const int &m = 43; //正确! c++编译器 会 分配内存空间 等同于 const int m = 43;
2-inline 内联函数
为了解决一些频繁调用的小函数大量占用栈空间(栈内存)的问题,因此引入了内联函数。
用法:在函数前面加上关键字 inline 即可
注意:
- inline必须和函数体的实现写在一块,声明的时候可以不写。
- 编译器可能拒绝inline的请求,而变成普通函数。
- inline函数编译限制:不能存在任何形式的循环语句(否则变成普通函数)
3-函数默认参数
- 在函数形参不写的时候,就使用默认参数。
void myPrint(int x = 3)
{
cout<<"x: "<<x<<endl;
}
main()
{
myPrint(4);
myPrint();
}
- 函数形参中如果有多个默认参数,和非默认参数,那默认参数应该写在右边:
void myPrint2( int m, int n, int x = 3, int y = 4)
错:void myPrint2( int m, int n, int x = 3, int y )
{
cout<<"x"<<x<<endl;
}
4-函数占位参数
函数调用时,必须写上所有参数
void func1(int a, int b, int)
{
cout<<"a"<<a<<" b"<<b<<endl;
}
void main()
{
//func1(1, 2); //err调用不起来
func1(1, 2, 3);
}
5-默认参数 & 占位参数
void func2(int a, int b, int =0)
{
cout<<"a= "<<a<<"\nb= "<<b<<endl;
}
int main()
{
func2(1, 2); //0k
func2(1, 2, 3); //ok
}
6-函数重载
用同一个函数名定义不同的函数,当函数名和不同的参数搭配时函数的含义不同。
函数重载至少满足下面的一个条件:(返回值不是重载的标准)
- 参数个数不同
- 参数类型不同
- 参数顺序不同
7-函数指针 & 指针函数
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。
-
指针函数:
其本质是一个函数,而该函数的返回值是一个指针。
int *fun(int x,int y); 其返回值是一个 int 类型的指针,是一个地址。
-
函数指针:
其本质是一个指针变量,该指针指向这个函数。
声明函数指针: int (*fun)(int x,int y);
函数指针是需要把一个函数的地址赋值给它,有两种写法:
p = &Function; p = Function; 取地址运算符&不是必需的,因为一个函数标识符就表示了它的地址。
如果是函数调用,还必须包含一个圆括号括起来的参数表。
x = (*fun)(); //推荐这种 x = fun();
8-类的封装 和 访问控制
封装:1-是对属性和方法进行封装;
2-对属性和方法进行访问控制(public/private/protected)
(把属性和方法进行封装 对属性和方法进行访问控制)
类比结构体有用,因为它不仅可以调用成员属性,还可以调用成员函数(方法)。
-
函数中调用类的方法:
class MyCircle{...} void printCircle01(MyCircle *pC) //指针 { cout<< "r" << pC->getR() << endl; cout<< "s" << pC->getS() << endl; } void printCircle02(MyCircle &myc) //引用 { cout<<myc.getS()<<endl; } void printCircle03(MyCircle myc) //类似结构体 { 。。。 }
类的访问控制关键字:
把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
public: 修饰的成员变量和函数 可以在类的内部和类的外部访问;
private: 修饰的成员变量和函数 只能在类的内部被访问,不能在类的外部访问;
protected: 修饰的成员变量和函数 只能在类的内部被访问,不能在类的外部访问, 用在继承里面。
public 相当于名字,可以任意被访问;
private 相当于密码,不能被任意访问。
Day_10
1-构造函数
构造函数我理解为可以快速初始化成员属性。在定义类时, 只要你写了构造函数,则必须要用
定义:
1)C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数;
2)构造函数在定义时可以没有参数,也可以有参数;
3)构造函数没有任何返回类型的声明。
1.1-无参数的构造函数调用
啥也不写
class Test
{
public:
Test() //无参数 构造函数
{
cout<<"我是构造函数 被执行了"<<endl;
}
};
int main()
{
Test2 t1; //调用无参数构造函数。
}
1.2-有参数的构造函数有3种调用方法:
class Test
{
public:
Test2(int a, int b) //有参数构造函数 //3种方法
{
m_a = a;
m_b = b;
cout<<"有参数构造函数"<<endl;
}
private:
int m_a;
int m_b;
};
int main()
{
1括号法 用的最多
Test2 t1(1, 2); //调用参数构造函数 c++编译器自动的调用构造函数
2 =号法 (我调试没弄出来)
Test2 t2 = (3, 4, 5, 6, 7); // = c+对等号符 功能增强 c++编译器自动的调用构造函数(虽然有多个逗号,但是只取了最右边两个)
3 直接调用构造函数 手动的调用构造函数
Test2 t4 = Test2(1, 2); //匿名对象 (匿名对象的去和留) 抛砖 ....//t4对象的初始化
}
1.3-拷贝(copy)构造函数的调用
拷贝构造函数: 用1个对象 去初始化另外一个对象 。
拷贝构造函数也是构造函数。
class Test4
{
public:
Test4() //无参数构造函数
{
m_a = 0;
m_b = 0;
cout<<"无参数构造函数"<<endl;
}
Test4(int a, int b) //有参数构造函数 //3种方法
{
m_a = a;
m_b = b;
cout<<"有参数构造函数"<<endl;
}
拷贝构造函数 (copy构造函数):用一个对象初始化另一个对象。
Test4(const Test4& obj ) //obj可写成其他a123
{
cout<<"我也是构造函数 " <<endl;
m_b = obj.m_b + 100;
m_a = obj.m_a + 100;
这里面还可以写其它操作
}
public:
void printT()
{
cout<<"普通成员函数"<<endl;
cout<<"m_a"<<m_a<<" m_a"<<m_b<<endl;
}
private:
int m_a;
int m_b;
};
int main()
{
Test4 t1(1, 2);
Test4 t0(1, 2);
t0 = t1;
//这是赋值=操作,不会 调用构造函数! 用t1给t0赋值 到操作 和 初始化是两个不同的概念。
第1种调用方法
Test4 t2 = t1; //用t1来初始化 t2 (拷贝构造和上面的赋值操作不一样!)
t2.printT();
第2种调用时机 一般推荐这个!
Test4 t1(1, 2);
Test4 t0(1, 2);
Test4 t2(t1); //用t1对象 初始化 t2对象
t2.printT();
}
1.4-默认构造函数
1)默认无参构造函数
当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空
2)默认拷贝构造函数
当类中没有定义拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值复制
2-析构函数
语法:~ClassName()
与类名相同,前面多了个 ~
1)C++中的类可以定义一个特殊的成员函数清理对象,这个特殊的成员函数叫做析构函数
2)析构函数没有参数也没有任何返回类型的声明
3)析构函数在对象销毁时(调用结束)自动被调用
4)析构函数调用机制:C++编译器自动调用
class Test
{
public:
~Test() //析构函数
{
cout<<"我是析构函数,被调用了" <<endl;
}
};
3-为什么要 构造函数 和 析构函数 ?
4-深拷贝 & 浅拷贝
浅拷贝 容易出现的问题:两个指针对象指向同一块内存空间,当其中一个指针对象释放该空间,另一个指针对象准备释放的时候就会出问题。析构两次造成程序内存泄露。
Name obj2 = obj1;
解决方法:手动自己写copy构造函数,使用**深拷贝**.给指针分配内存空间
class Name{
...
Name(const Name& obj1)
{
m_len = obj1.m_len;
m_p = (char *)malloc(m_len + 1);
strcpy(m_p, obj1.m_p);
}
...
};
int main()
{
...
Name obj2 = obj1;
...
}
5-构造函数的初始化列表
解决: 在B类中 组合了一个 A类对象 (A类设计了构造函数)。
根据构造函数的调用规则 设计A的构造函数, 必须要用。但是没有机会初始化A,所以使用了新的语法:
Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3)
class A
{
public:
A(int _a)
{
a = _a;
cout << "构造函数" << "a" << a << endl;
}
~A()
{
cout << "析构函数" << "a" << a << endl;
}
protected:
private:
int a;
};
class B
{
public:
B(int _b1, int _b2) : a1(1), a2(2), c(0)
{
}
B(int _b1, int _b2, int m, int n) : a1(m), a2(n), c(0)
{
b1 = _b1;
b2 = _b2;
cout <<"B的构造函数"<<endl;
}
~B()
{
cout<<"B的析构函数" <<endl;
}
protected:
private: //以后在写的时候先写别的类的数据,再写自己的
A a1;
A a2;
int b1;
int b2;
const int c; //类成员中若有const修饰,必须在对象初始化的时候,给const int c 赋值
};
int main()
{
//A a1(10);
//B ojbB(1, 2);
//1参数传递
B ojbB2(1, 2,3, 4); //这里 1 2 先给 A a2 a1了,然后 3 4 给b1 b2
//2 调用顺序
return ;
}
解释上面 参数传递:
构造函数与析构函数的调用顺序
1)当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同;之后调用自身类的构造函数。
2)析构函数的调用顺序与对应的构造函数调用顺序相反。
6- new 、 delete 基本语法
6.1-分配基础类型
在C语言中:
int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);
C++是这样:
int *p2 = new int; //分配基础类型
*p2 = 20;
free(p2);
或:
int *p3 = new int(30); //直接初始化
delete p3;
6.2-分配数组变量
在C语言中:
int *p = (int *)malloc(sizeof(int) * 10); //int array[10];
p[0] = 1;
free(p);
c++分配数组 :
int *pArray = new int[10] ;
pArray[1] = 2;
delete [] pArray; //数组不要把[] 忘记
char *pArray2 = new char[25] ; //char buf[25]
delete [] pArray2;
6.3-分配对象
class Test
{
public:
Test(int _a)
{
a = _a;
cout<<"构造函数执行" <<endl;
}
~Test()
{
cout<<"析构函数执行" <<endl;
}
protected:
private:
int a;
};
在C语言中:(C语言中没有 class 哈!)
Test *pT1 = (Test *)malloc(sizeof(Test));
free(pT1);
C++是这样:
Test *pT2 = new Test(10);
delete pT2;
6.4- new、delete 和 malloc 、free不同的地方:
new能调用类的构造函数 delete操作符 能执行类的析构函数。
这也是C艹的特色!
7-new、delete 深入分析
7.1-用 malloc分配的指针变量可以用 delete消除;
同理,用 new分配的指针变量可以用free消除;
7.2-malloc 是C语言的内存分配函数,在使用的时候只会分配内存大小,但是不会调用类的构造函数(而new可以);delete同理。
毕竟 C 语言没有 class 嘛!
8- 静态成员变量, 和静态成员函数
8.1-静态成员变量
关键字 static 可以用于说明一个类的成员。把一个类的成员说明为 static 时,这个类无论有多少个对象被创建,这些对象共享这个 static 成员。
在main() 外 要先定义静态成员变量!
int X::s=0;
注意:a中的 s变了后,bcd也相应的变了!
8.2-静态成员函数
声明:(里面只能调用静态成员变量,不然的话系统分不清)
static void getS() //静态成员函数
{
cout<<"S: "<<s<<endl;
cout<<"a:"<<a<<endl; //error C2597: 对非静态成员“BB::a”的非法引用
}
调用:
int main()
{
法1: 用 对象. 调用
b.getS();
法2:由于是静态量,在所有类中是公用的,于是可以由类名调用
X::getS();
}
【问】请在静态成员函数中,能调用 普通成员属性 或者 普通成员函数吗?(如上 a )
不能!
9-C++面向对象模型初探
C++类对象中的成员变量和成员函数是分开存储的
- 成员变量:
- 普通成员变量:存储于对象中,与struct变量有相同的内存布局和字节对齐方式
- 静态成员变量:存储于全局数据区中
- 成员函数:存储于代码段中。
9.1 C++类对象中的成员变量和成员函数是分开存储的。C语言中的内存四区模型仍然有效!
9.2 C++中类的普通成员函数都隐式包含一个指向当前对象的this指针。
9.3 静态成员函数、成员变量属于类
静态成员函数与普通成员函数的区别
静态成员函数不包含指向具体对象的指针
普通成员函数包含一个指向具体对象的指针
10- this 指针
在 class中使用的很方便。特别是在构造函数中传值给成员变量的时候;
谁调用,this就指谁(这里指t1)。
class Test
{
public:
Test(int a, int b) //---> Test(Test *this, int a, int b)
{
this->a = a;
this-> b = b;
}
void printT()
{
cout<<"a: " <<a <<endl;
cout<< "b: " << this->b <<endl;
}
protected:
private:
int a;
int b;
};
main()
{
Test t1(1, 2);
t1.printT();// ===> printT(&t1)
}
Day_11
1- 在 class 中 写的const 修饰的是谁
在 class 中的成员函数可能遇到这样写的:
void const OpVar( int a, int b)
//或一般把const写在后面:
void OpVar( int a, int b) const
//==>void OpVar(const Test *this, int a, int b)
//==>void OpVar( const Test *const this, int a, int b)
{
a = 100;
//this->a = 100;
//this->b = 200;
//this = 0x11;
//cout<<"a: " <<a <<endl;
cout<< "b: " << this->b <<endl;
}
在实验中发现 形参 a b 都可以修改,但是 this->a this->b不可以被修改!
const修饰的是this指针所指向的内存空间,不能修改(类似于 const 出现在星号左边的情况)。
如果关键字 const 出现在星号左边,表示被指物是常量;(只能读,不能修改)。
2-友元
2.1-友元函数
友元函数 突破了类的封装性。
class A
{
public:
友元函数声明的位置 和 public private没有关系
friend void modifyA(A *pA, int _a); //2 函数modifyA 是 类A的好朋友
A(int a=0, int b=0)
{
this->a = a;
this->b = b;
}
int getA()
{
return this->a;
}
private:
int a;
int b;
};
void modifyA(A *pA, int _a)
{
//pA->a = 100;
pA->a = _a;
}
int main()
{
A a1(1, 2);
...
modifyA(&a1, 300);
}
2.2-友元类
若B类是A类的友员类,则B类的所有成员函数都是A类的友员函数。
友员类通常设计为一种对数据操作或类之间传递消息的辅助类 。
【理解】在 class A 中定义了 friend class B,那么B就可以随便访问A这个朋友了。
class A
{
public:
friend class B;//B类 是 A的好朋友 ,在B中可以访问A类的私有成员 私有函数
A(int a=0, int b=0)
{
this->a = a;
this->b = b;
}
int getA()
{
return this->a;
}
private:
int a;
int b;
};
class B
{
public:
void Set(int a)
{
Aobject.a = a;
}
void printB()
{
cout<<Aobject.a <<endl;
}
private:
A Aobject;
};
main()
{
B b1;
b1.Set(300);
b1.printB();
}
3-运算符重载
3.1-入门基础
关键字 operator
**所谓重载,就是重新赋予新的含义。运算符重载的本质 是 函数调用。**函数重载就是对一个已有的函数赋予新的含义,使之实现新功能,因此,一个函数名就可以用来代表不同功能的函数,也就是”一名多用”。
(如果想把 a b 写在 private 里面,就要定义友元函数 AA operator+(AA &c1, AA &c2) )
class AA
{
public:
int a;
int b;
public:
AA(int a=0, int b=0)
{
this->a = a;
this->b = b;
}
void printCom()
{
cout<<a<<" + " << b << "i" <<endl;
}
};
AA operator+(AA &c1, AA &c2)
{
cout<<"12345上山 打老虎"<<endl;
AA tmp(c1.a + c2.a, c1.b+ c2.b);
return tmp; //
}
这里似乎是定义了一个函数,函数名为“operator+”,但是在主函数中调用:(这个时候程序已经认识了 + 号)
AA c4 = c1 + c2;
时 函数默认你用的是 operatpr+, 从而让
c1.a + c2.a, c1.b+ c2.b
这就是函数重载。(不定义 operator+ 的时候就不行。)
3.2-运算符重载语法理论知识介绍
限制:
3.3-二元运算符重载
二元运算符重载 分为 重载为成员函数 和 重载为友元函数。
这个比较简单(注意,对private里面的数据调用的时候 记得把函数声明为友元函数。故称为友元函数重载)
3.4- 一元运算符重载
eg.前置++ – 和 后置++ –
class Complex
{
private:
int a;
int b;
//重载 前置++
friend Complex& operator++(Complex &c1);
friend Complex operator++(Complex &c1, int);
public:
Complex(int a=0, int b=0)
{
this->a = a;
this->b = b;
}
void printCom()
{
cout<<a<<" + " << b << "i" <<endl;
}
public:
//成员函数法 实现 -运算符重载
Complex operator-(Complex &c2)
{
Complex tmp(this->a - c2.a, this->b - c2.b);
return tmp;
}
//前置--
Complex& operator--()
{
this->a --;
this->b --;
return *this;
}
//后置--
Complex operator--(int)
{
Complex tmp = *this;
this->a--;
this->b--;
return tmp;
}
};
//全局函数法 实现 + 运算符重载
Complex operator+(Complex &c1, Complex &c2)
{
Complex tmp(c1.a + c2.a, c1.b + c2.b);
return tmp;
}
//前置++
Complex& operator++(Complex &c1)
{
c1.a++;
c1.b++;
return c1;
}
//后置++
Complex operator++(Complex &c1, int) //这里的 int 是一个占位符,为了避免函数重载,
//系统不知道调用哪一个函数
{
//先使用 在让c1加加
Complex tmp = c1;
//return c1;
c1.a ++;
c1.b ++;
return tmp;
}
/*
全局函数、类成员函数方法实现运算符重载步骤
1)要承认操作符重载是一个函数,写出函数名称
2)根据操作数,写出函数参数
3)根据业务,完善函数返回值(看函数是返回引用 还是指针 元素),及实现函数业务
*/
void main()
{
//前置++操作符 用全局函数实现
++c1;
c1.printCom();
//前置--操作符 成员函数方法
--c1;
c1.printCom();
//Complex& operator++(Complex &c1)
//c1.operator--();
//后置++操作符 用全局函数实现
c1++;
c1.printCom();
//后置--操作符 用成员函数实现
c1--;
c1.printCom();
//c1.operator--()
}
4-函数返回值当左值,需要返回一个引用
在很多代码中都有体现了!
返回的是一个引用(元素本身),而不是一个值。
Day_12
1-智能指针类编写
3.1问题抛出
指针使用过程中,经常会出现内存泄漏和内存多次被释放常
3.2 解决方案:例如:boost库的智能指针
项目开发中,要求开发者使用预先编写的智能指针类对象代替C语言中的原生指针
3.3 智能指针思想
工程中的智能指针是一个类模板
通过构造函数接管申请的内存
通过析构函数确保堆内存被及时释放
通过重载指针运算符* 和 -> 来模拟指针的行为
通过重载比较运算符 == 和 != 来模拟指针的比较
4- MyString 类
4.1-构造函数 和 析构函数
Day_13
1-继承
面向对象程序设计有4个主要特点:抽象、封装、继承和多态性。
1.1-类和类的关系
has-A,uses-A 和 is-A 分别为 包含关系、友元关系、继承关系
1.2-派生类的定义
- 子类拥有父类的所有 成员变量 和 成员函数
- 子类可以拥有父类没有的方法和属性
- 子类就是一种特殊的父类
- 子类对象可以当作父类对象使用
1.3-单个类的访问控制
“三看”原则:
C++中的继承方式(public、private、protected)会影响子类的对外访问属性。判断某一句话,能否被访问
1)看调用语句,这句话写在子类的内部、外部
2)看子类如何从父类继承(public、private、protected)
3)看父类中的访问级别(public、private、protected)
class Parent
{
public:
int a; //老爹的名字
protected:
int b; //老爹的银行密码(有时候可以访问)
private:
int c; //老的情人
};
public :修饰的成员变量 方法 在类的内部 类的外部都能使用。
protected: 修饰的成员变量方法,在类的内部使用 ,在继承的子类中可用 ;其他 类的外部不能被使用。
private: 修饰的成员变量方法 只能在类的内部使用 不能在类的外部。
派生类访问控制的结论:
1 protected 关键字 修饰的成员变量 和成员函数 ,是为了在家族中使用 ,是为了继承
2 项目开发中 一般情况下 是 public。
2-继承中的 构造 和 析构
2.1-类型兼容性原则
//样例,下面有说明
class Parent //基类
{
public:
void printP()
{
cout<<"我是爹..."<<endl;
}
Parent()
{
cout<<"parent构造函数"<<endl;
}
Parent(const Parent &obj)
{
cout<<"copy构造函数"<<endl;
}
private:
int a;
};
class child : public Parent //子类
{
public:
void printC()
{
cout<<"我是儿子"<<endl;
}
protected:
private:
int c;
};
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。类型兼容规则中所指的替代包括以下情况:
- 子类对象 可以当作 父类对象 使用
- 子类对象 可以直接赋值给 父类对象
- 子类对象 可以直接初始化 父类对象
//子类就是一个特殊的父类
Parent p3 = c1;
- 父类指针 可以直接指向 子类对象
child c1;
Parent *p = NULL;
p = &c1;
p->printP(); //有什么意义?不是自己指向了自己么?
-
父类引用 可以直接引用 子类对象
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。类型兼容规则是多态性的重要基础之一。
2.2-继承中的构造与析构
class Parent
{
public:
Parent(int a, int b)
{
this->a = a;
this->b = b;
cout<<"父类构造函数..."<<endl;
}
~Parent()
{
cout<<"析构函数..."<<endl;
}
void printP(int a, int b)
{
this->a = a;
this->b = b;
cout<<"我是爹..."<<endl;
}
private:
int a;
int b;
};
class child : public Parent
{
public:
child(int a, int b, int c) : Parent(a, b) //这里用了构造函数的初始化列表。
{
this->c = c;
cout<<"子类的构造函数"<<endl;
}
~child()
{
cout<<"子类的析构"<<endl;
}
void printC()
{
cout<<"我是儿子"<<endl;
}
protected:
private:
int c;
};
void main()
{
child c1(1, 2, 3);
c1.printc();
c1.Printp();
}
第 30 行用了 构造函数的初始化列表,对父类也进行了初始化。 若写成 :
child(int c,int a, int b) : Parent(a, b)
输出的结果就是 1 2 3
继承中的构造析构调用原则
- 子类对象在创建时会首先调用 父类的构造函数;
- 父类构造函数执行结束后,执行子类的构造函数。
- 当父类的构造函数有参数时,需要在子类的初始化列表中显示调用;
- 析构函数调用的先后顺序与构造函数相反。
2.3-继承中同名成员变量处理方法
当子类成员变量与父类成员变量同名时,子类依然从父类继承同名成员。
在子类中通过 作用域分辨符:: 进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符) 。同名成员存储在内存中的不同位置。
3-多继承
3.1-基本知识
一个类有多个直接基类的继承关系称为多继承。
4-继承总结
-
继承是面向对象程序设计实现软件重用的重要方法。程序员可以在已有基类的基础上定义新的派生类。
-
单继承的派生类只有一个基类。多继承的派生类有多个基类。
-
派生类对基类成员的访问由继承方式和成员性质决定。
-
创建派生类对象时,先调用基类构造函数初始化派生类中的基类成员。调用析构函数的次序和调用构造函数的次序相反。
-
C++提供虚继承机制,防止类继承关系中成员访问的二义性。
多继承的二义性 指的是:有一个 爷爷类,它有两个 爸爸子类,用一个儿子类同时继承这两个爸爸类,在调用爷爷类的数据的时候,编译器不知道调用的是A爸爸的爸爸还是 B爸爸的爸爸。**解决方法:**在两个爸爸类声明的时候 加上 virtual 关键字:
class b1 : vitrual public B {...}
-
多继承提供了软件重用的强大功能,也增加了程序的复杂性。
5-多态
面向对象三大概念、三种境界(封装、继承、多态)
- 封装:突破了C语言函数的概念,当把一个类做函数参数的时候,这个时候能把一个类的对象传到函数里面,在函数中可以使用对象的属性 和对象的方法;
- 继承:可以复用其他人所写的方法,能够提高工作效率;
- 多态:不仅能继承前人写的方法,还能复用后人写的代码。(这是一种更高层次的思想)。
实现多态三个条件:
- 要有继承
- 要有虚函数重写(父类 和 子类函数同名)(函数前面出现 virtual就是虚函数)
- 用父类指针(父类引用)指向子类对象…
5.1-面向对象新需求
根据实际的对象类型来判断重写函数的调用:
- 如果父类指针指向的是 父类对象 则调用父类中定义的函数
- 如果父类指针指向的是 子类对象 则调用子类中定义的重写函数
5.2-解决方案:
C++中通过 virtual 关键字对多态进行支持。使用virtual声明的函数被重写后即可展现多态特性。
在相同的函数名前面,父类写了 virtual 后,子类可写可不写(最好写上)。
class Parent
{
public:
Parent(int a)
{
this->a = a;
cout<<"Parent a"<<a<<endl;
}
virtual void print() //子类的和父类的函数名字一样
{
cout<<"Parent 打印 a:"<<a<<endl;
}
protected:
private:
int a ;
};
class Child : public Parent
{
public:
Child(int b) : Parent(10)
{
this->b = b;
cout<<"Child b"<<b<<endl;
}
virtual void print() /
{
cout<<"Child 打印 b:"<<b<<endl;
}
protected:
private:
int b;
};
void main()
{
Parent *base = NULL;
Parent p1(20);
Child c1(30);
base = &p1;
base->print(); //执行父类的打印函数
base = &c1;
base->print(); //执行谁的函数 ? //面向对象新需求
}
如果没有多态,那么当 base执行print函数的时候永远执行的是父类的。有了多态后,当 base指向 子类的时候,当遇到同名函数时也会对应的使用子类。
5.3-虚析构函数
想通过父类指针 把 所有的子类对象的析构函数 都执行一遍。
使用方法:在父类的析构函数前面加上 virtual
class A
{
public:
A()
{
p = new char[20];
strcpy(p, "obja");
printf("A()\n");
}
virtual ~A()
{
delete [] p;
printf("~A()\n");
}
protected:
private:
char *p;
};
class B : public A
{
public:
B()
{
p = new char[20];
strcpy(p, "objb");
printf("B()\n");
}
~B()
{
delete [] p;
printf("~B()\n");
}
protected:
private:
char *p;
};
class C : public B
{
public:
C()
{
p = new char[20];
strcpy(p, "objc");
printf("C()\n");
}
~C()
{
delete [] p;
printf("~C()\n");
}ete(myC);
}
功能:通过父类指针 释放所有的子类资源。
如果将父类 A 设置为虚析构函数,在该函数执行完后,只能析构A,并不能对 B C类进行析构释放资源。(另一种解决方法就是一个一个的 delete 释放资源)。
最简单的还是把 祖宗类 的析构函数设置为虚析构函数。
6-重载重写重定义
7-多态相关面试题
7.1-请谈谈你对多态的理解
- 多态的实现效果
多态:同样的调用语句有多种不同的表现形态; - 多态实现的三个条件
有继承、有virtual重写、有父类指针(引用)指向子类对象。 - 多态的C++实现
virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用。 - 多态的理论基础
动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。 - 多态的重要意义
设计模式的基础 是框架的基石。 - 实现多态的理论基础
函数指针做函数参数;
C函数指针是C++至高无上的荣耀。C函数指针一般有两种用法(正、反)。 - 多态原理探究
与面试官展开讨论。
7.2-谈谈C++编译器是如何实现多态
c++编译器多态实现原理
当类中声明虚函数时,编译器会在类中生成一个虚函数表
虚函数表是一个存储类成员函数指针的数据结构
虚函数表是由编译器自动生成与维护的
virtual成员函数 会被编译器放入虚函数表中
当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、子类对象提前布局 vptr指针 ;当进行howToPrint(Parent *base)函数是,C++编译器不需要区分子类对象或者父类对象,只需要再base指针中,找vptr指针即可。)
VPTR一般作为类对象的第一个成员
7.3-谈谈你对重写,重载理解
-
函数重载
必须在同一个类中进行
子类无法重载父类的函数,父类同名函数将被名称覆盖
重载是在编译期间根据参数类型和个数决定函数调用
-
函数重写
必须发生于父类与子类之间
并且父类与子类中的函数必须有完全相同的原型
使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
多态是在运行期间根据具体对象的类型决定函数调用
7.4-是否可以将类的每个成员函数都声明为虚函数,为什么。
c++编译器多态实现原理
可以,但没必要,浪费内存。
7.5-构造函数中调用虚函数能实现多态吗?为什么?
c++编译器多态实现原理
构造函数汇总能调用虚函数,实现多态吗?
1)-对象中的VPTR指针什么时候被初始化?
对象在创建的时,由编译器对VPTR指针进行初始化
只有当对象的构造完全结束后VPTR的指向才最终确定
父类对象的VPTR指向父类虚函数表
子类对象的VPTR指向子类虚函数表
7.6-虚函数表指针(VPTR)被编译器初始化的过程,你是如何理解的?
c++编译器多态实现原理
7.7-父类的构造函数中调用虚函数,能发生多态吗?
c++编译器多态实现原理
7.8-为什么要定义虚析构函数?
在什么情况下应当声明虚函数
- 构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数
- 析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象
Day_14
1-多态原理探究
1.1-多态成立的三个条件
要有继承 、 虚函数重写 、父类指针指向子类对象
1.2-VPTR指针(虚函数表指针)
为什么在父类指针指向子类对象的时候编译器知道使用哪个多态函数?
用类定义对象的时候,C++编译器会在对象中添加一个 VPTR指针,用来把类中的虚函数做成虚函数表(存储多个虚函数的入口地址)。
父类对象和子类对象分别有vptr指针 , 根据 VPTR指针 去找==>虚函数表===>函数的入口地址。==>实现多态!
所以C++编译器根本不需要区分是子类对象 还是父类对象
效果:
传来子类对 执行子类的print函数 传来父类对执行父类的print函数
1.3-子类的 VPTR指针是如何分布初始化的
2-抽象类 和 纯虚函数
面向抽象类编程(面向一套预先定义好的接口编程)
解耦合 ....模块的划分
- 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本。
- 纯虚函数为各派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)。
- 表达形式:
virtual 类型 函数名(参数表) = 0 ;
- 含有纯虚函数的类叫 抽象类;抽象类就相当于一个接口。
- 【注】约定一个统一的界面(接口),让子类使用,让子类必须去实现.
抽象类不能建立对象,但是可以声明抽象类的指针。
//面向抽象类编程(面向一套预先定义好的接口编程)
//解耦合 ....模块的划分
class Figure //抽象类
{
public:
//约定一个统一的界面(接口),让子类使用,让子类必须去实现
virtual void getArea() = 0 ; //定义纯虚函数
protected:
private:
};
class Circle : public Figure
{
public:
Circle(int a, int b)
{
this->a = a;
this->b = b;
}
virtual void getArea() //子类可不写 virtual
{
cout<<"圆形的面积: "<<3.14*a*a<<endl;;
}
private:
int a;
int b;
};
class Tri : public Figure
{
public:
Tri(int a, int b)
{
this->a = a;
this->b = b;
}
virtual void getArea()
{
cout<<"三角形的面积: "<<a*b/2<<endl;;
}
private:
int a;
int b;
};
class Square : public Figure
{
public:
Square(int a, int b)
{
this->a = a;
this->b = b;
}
virtual void getArea()
{
cout<<"四边形的面积: "<<a*b<<endl;;
}
private:
int a;
int b;
};
void objplay(Figure *base)
{
base->getArea(); //会发生多态
}
void main511()
{
//Figure f; //err 抽象类不能被实例化
Figure *base = NULL; //抽象类不能被实例化
Circle c1(10, 20);
Tri t1(20, 30);
Square s1(50, 60);
//面向抽象类编程(面向一套预先定义好的接口编程)
objplay(&c1);
objplay(&t1);
objplay(&s1);
}
3-多继承
一般是使用抽象类在多继承的使用。
写几个抽象类(相当于写了几个接口),然后写一个 parent类,在 child类中继承这些类:
class Child : public Parent, public Interface1, public Interface2
{
...
};
在使用的时候:
main()
{
Child c1;
c1.print(); //常规操作,未使用多态和多继承
Interface1 *it1 = &c1; //使用了第一个抽象类,it1是该抽象类的指针。
it1->add(1, 2);
Interface2 *it2 = &c1;
it2->mult(3, 6);
}
4-(重点)面向抽象类编程思想强化
理论知识
虚函数和多态性使成员函数根据调用对象的类型产生不同的动作
多态性特别适合于实现分层结构的软件系统,便于对问题抽象时 定义共性,实现时定义区别
面向抽象类编程(面向接口编程)是项目开发中重要技能之一。
在程序最开头写上
#pragma once
避免调用的其它多个 .h文件里面的重复类。这样就只调用一次了。
Day_15
1-数组指针语法梳理
-
定义一个数组类型
typedef int (myTypeArray)[10]; myTypeArray myArray; myArray[0] = 10; printf("%d \n", myArray[0]);
-
定义一个指针数组类型
typedef int (*PTypeArray)[10]; //int *p PTypeArray myPArray; //sizeof(int) *10 myPArray = &a; //int b = 10; //int *p = NULL; //p = &b; (*myPArray)[0] = 20; printf("a[0]: %d \n", a[0]);
-
定义一个指向 数组类型的指针(数组指针)
int (*MyPointer)[10]; //变量 告诉C编译器 给我分配内存 MyPointer = &a; (*MyPointer)[0] = 40; printf("a[0]: %d \n", a[0]);
2-函数指针语法梳理
-
如何定义一个函数类型
typedef int (MyFuncType)(int a, int b); //定义了一个类型 MyFuncType *myPointerFunc = NULL; //定义了一个指针, 指向某一种类的函数.. myPointerFunc = &add; //细节 myPointerFunc(3, 4); //间接调用 myPointerFunc = add; //细节 //C 过程 兼容历史版本的原因 myPointerFunc(3, 4); //间接调用
-
如何定义一个函数指针类型
typedef int (*MyPointerFuncType)(int a, int b); //int * a = NULL; MyPointerFuncType myPonterFunc; //定义一个指针 myPonterFunc = add; myPonterFunc(5, 6);
-
如何定义一个 函数指针 (指向一个函数的入口地址)
int (*MyPonterFunc)(int a, int b); //定义了一个变量 MyPonterFunc = add; MyPonterFunc(7, 8);
3-面向对象3大概念
封装:
突破c函数的概念…用类做函数参数的时候,可以使用对象的属性 和对象的方法
继承:
A B 代码复用
多态 :
可以使用未来…
实现多态三个条件:
- 要有继承
- 要有虚函数重写(父类 和 子类函数同名)(函数前面出现 virtual就是虚函数)
- 用父类指针(父类引用)指向子类对象…