C++派生 虚函数 模板
后端码匠 人气:0继承与派生
C ++ 是面向对象编程,那么只要面向对象,都会有多态、继承的特性。C++是如何实现继承的呢?
继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。
在C++中,派生(Derive) 和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。
被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。
在C++中继承称为派生类,基类孵化除了派生类,使用:来表示子类继承父类,C++中支持多继承,使用逗号分隔
class Parent { public: int name; protected: int code; private: int num; }; class Parent1 { }; // C++中,:表示继承,可以多继承逗号分隔 // public/protected/private继承,对于基类起到一些保护机制 默认是private继承 class Child : public Parent, Parent1 { void test() { // 派生类可以访问到public属性和protected属性 this->name; this->code; } };
C++中派生类中添加了public 派生、protected派生、private派生,默认是private派生
class 派生类名:[继承方式] 基类名{ 派生类新增加的成员 };
class Parent { public: int name; protected: int code; private: int num; }; class Parent1 { }; // private私有继承 class Child1 : private Parent { void test() { this->name; this->code; } }; // protected继承 class Child2 : protected Parent { void test() { this->name; this->code; } };
public 派生、protected派生、private派生对于,创建的对象调用父类的属性和方法起到了限制和保护的作用
Child child; child.name; // public继承。调用者可以访问到父类公有属性,私有属性访问不到的 Child1 child1; // child1.name; // private继承.调用者访问不到父类公有属性和私有属性 Child2 child2; // child2.name; // protected继承,调用者访问不到父类公有属性和私有属性
虚函数
重点!!! C++的继承和java中的继承存在的不同点: 基类成员函数和派生类成员函数不构成重载
基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
父类代码如下
#include <cstring> #include <iostream> using namespace std; class Person { protected: char *str; public: Person(char *str) { if (str != NULL) { this->str = new char[strlen(str) + 1]; strcpy(this->str, str); } else { this->str = NULL; } cout << "parent" << endl; } Person(const Person &p) { cout << "copy parent" << endl; } void printC() { cout << "parent printC" << endl; } ~Person() { // if (str != NULL) { // delete[] str; // 如果调用了这个方法只会调用一次析构函数 // } // cout << "parent destroy" << endl; } };
子类继承父类,并且调用父类的构造函数, 通过:来调用父类的构造函数
// 子类 class CTest : public Person { public: // 调用父类的构造方法 CTest(char *str) : Person(str) { cout << "child" << endl; } void printC() { cout << "child printC" << endl; } ~CTest() { cout << "child destroy " << endl; } };
在C++中和Java的不同在于如下代码:只要是父类的指针都是调用的父类的方法,哪怕子类对象直接赋值给父类,也会调用父类的方法,而不会调用子类的方法。
int main() { Person person = CTest("jake"); person.printC(); // parent printC cout << "-----------" << endl; Person *p = NULL; CTest c1("123"); p = &c1; c1.printC(); // child printC p->printC(); // parent printC 为什么会调用的是parent的方法呢? return 0; }
parent
child
copy parent
child destroy
parent printC
-----------
parent
child
child printC
parent printC
child destroy
哪怕通过指针传递和引用传递,只要使用的父类都会调用父类的方法
// 通过指针传递只会调用父类的方法,不会调用子类的方法 void howToPaint(Person *p) { p->printC(); } // 通过引用类型,只会调用父类的方法,不会调用子类的方法 void howToPaint1(Person &p) { p.printC(); }
cout << "---------" << endl; howToPaint(p); // parent printC howToPaint(&c1); // parent printC cout << "-------" << endl; Person p1("123"); // 都是父类的方法 howToPaint1(p1); // parent printC howToPaint1(c1); // parent printC cout << "--------" << endl; CTest c2("123"); Person p2 = c2; // 会不会调用父类的拷贝函数呢? copy parent 会进行调用
---------
parent printC
parent printC
-------
parent
parent printC
parent printC
--------
parent
child
copy parent
child destroy
child destroy
这是为什么呢?
C++ 中会按照函数表的顺序进行调用,很显然父类的函数是在子类函数的前面的
那么如何调用到子类的方法呢?C ++提供了虚函数的方式,虚函数也是实现多态的关键。
虚函数与纯虚函数,纯虚函数在java 中 abstract == 纯虚函数
实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。
包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
- 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
- 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。
- 基类的析构函数必须声明为虚函数。
#include <iostream> using namespace std; class Person { public: // 增加了一个虚函数表的指针 // 虚函数 子类可以覆写的函数 virtual void look() { cout << "virtual look" << endl; } // 纯虚函数 必须要让子类实现的 virtual void speak() {}; // 基类的析构函数必须声明为虚函数 virtual ~Person() { cout << "~Person" << endl; } }; class Child : public Person { public: // 子类实现纯虚函数 void speak() override { cout << "child speak" << endl; } // 访问父类的方法 void look() override { cout << "child look" << endl; Person::look(); } ~Child() { cout << "~Child" << endl; } }; int main() { Person *person = new Child(); // 必须通过指针的方式,不同通过栈的方式去派生抽象 person->speak(); // child speak person->look(); // child look Person p; cout << sizeof(p) << endl; // 8 这就表明了虚函数是有一个虚函数表,增加一个指针*vtable,指向了虚函数表 // 下面代码来证明 typedef void (*func)(void); func fun = NULL; cout << (int *) &p << endl; // 指向函数的首地址 0x16ee1efa8 cout << (int *) *(int *) &p << endl; // 函数的地址 0xfe40a0 fun = (func) *((int *) *(int *) &p); fun(); // virtual look return 0; } /** * child speak * child look * virtual look * ~Child * ~Person */
child speak
child look
virtual look
8
0x16ee1efa8
0xfe40a0
模板
模板和java的泛型类似。 模板类不支持声明(.h)和实现(.cpp)分开写,「不能将模板的声明和定义分散到多个文件中」的根本原因是:模板的实例化是由编译器完成的,而不是由链接器完成的,这可能会导致在链接期间找不到对应的实例。
函数模板
#include <iostream> #include <string> #include <cstring> using namespace std; /** * 函数模板和java中的泛型类似 */ // 方法泛型 这里只能声明在方法上 template<typename T, typename R=int> // R的默认类型是int // typename == class 两个等价的 void swap2(T t, R r) { } template<typename T> void swapT(T &a, T &b) { cout << "swap: T a T b" << endl; T temp = a; a = b; b = temp; } // 普通函数优先级比泛型函数高,只有类型重合的状态下 void swapT(int &a, int &b) { cout << "swap : int a int b" << endl; int temp = a; a = b; b = temp; } int main() { // 函数模板 int a = 10; int b = 20; char c = 'a'; swapT<int>(a, b); // 显示调度 swapT(a, b); // 自动推导 // swap(a,c); // 报错 无法推导出具体的类型 // swap2(); // 报错 无法推导出具体的类型 char *a1 = "abc"; char *a2 = "123"; cout << a1 << a2 << endl; swapT(a1, a2); cout << a1 << a2 << endl; return 0; }
swap: T a T b
swap : int a int b
abc123
swap: T a T b
123abc
类模板
#include <iostream> #include <cstring> using namespace std; // 模板修饰在类上 template<typename T, typename R> class Person { public: T a; R b; Person(T t) { } T &getA() { T t1; // return t1; // 这里不可以返回,因为方法执行完毕后会销毁掉 return a; // 返回值是引用 } }; /** * 和java不同的部分,比java更加灵活 */ class Pp { public: void show() { cout << "Pp show" << endl; } }; template<typename T> class ObjTemp { private: T obj; public: void showPp() { // 自动检查 但是会出现不可预期的错误 obj.show(); // 假设模板是Pp,可以调用Pp的变量和方法,在java中需要<T extend Pp> T才能调用方法 } }; template<typename T, typename R> class CTest { public: T m_name; R m_age; CTest(T name, R age) { this->m_name = name; this->m_age = age; } void show() { cout << "show T:" << m_name << " R:" << m_age << endl; } }; template<typename T, typename R> void doWork(CTest<T, R> &cTest) { cTest.show(); } template<typename T> void doWork2(T &t) { t.show(); // 在java中必须是<T extend xxxx> } // 继承模板问题和java是一样的 template<typename T> class Base { public: T t; }; // 确定的类型或者模板 template<typename T, typename R> class Son : Base<R> { public: T t1; }; int main() { CTest<string, int> test("后端码匠", 28); // show T:后端码匠 R:28 doWork(test); doWork2<CTest<string, int>>(test); // 显示调用 doWork2(test); // 自动推导 ObjTemp<Pp> temp; temp.showPp(); // Pp show 可以调用传递过来的模板的方法 // 自动类型推导,在类模板上不可以使用,无法推导出具体的类型 Person<int, string> p(100); cout << p.getA() << endl; return 0; }
show T:后端码匠 R:28
show T:后端码匠 R:28
show T:后端码匠 R:28
Pp show
0
实现一个模板类ArrayList类似Java的列表实现:
注意在之前学习的.h和.cpp分开的方式,不支持模板,一般模板的部分都会合并到.h文件中。
#include <iostream> #include <cstring> using namespace std; #ifndef CPPDEMO_ARRAYLIST_H #define CPPDEMO_ARRAYLIST_H template<typename T> class ArrayList { public: int d = 11; ArrayList() { this->size = 16; this->realSize = 0; this->arr = new T[this->size]; } // explicit 不能通过隐式调用 explicit ArrayList(int capacity) { this->size = capacity; this->realSize = 0; // 在堆区申请数组 this->arr = new T[this->size]; // 在堆中开辟的一块空间 存储的是一个int[size] 数组,arr指向数组的首地址 } // 拷贝函数 ArrayList(const ArrayList &arrayList) { this->size = arrayList.size; this->realSize = arrayList.realSize; this->arr = new T[arrayList.size]; // 将数组的值赋值到arr中 for (int i = 0; i < this->size; ++i) { this->arr[i] = arrayList.arr[i]; // arrayList.arr[i]他也是指针 this->arr[i] 是指针 } } // 析构函数 ~ArrayList() { if (this->arr != nullptr) { delete[] this->arr; this->arr = nullptr; } } void add(T val) { add(val, this->realSize); } void add(T val, int index) { if (index < 0 || index > size) { return; } // 判断容量是否够大 不够进行扩容 if (this->realSize >= size * 0.75) { resize(); } this->arr[index] = val; // 等价于 *((this->arr)+index) = val this->realSize++; // 数据量大小+1 } T get(int index) { if (index < 0 || index >= realSize) { return -1; } return this->arr[index]; } T remove(int index) { if (index < 0 || index >= realSize) { return -1; } // 如何移除呢?循环往前移动 int result = this->arr[index]; for (int i = index; i < size - 1; ++i) { this->arr[i] = this->arr[i + 1]; } this->realSize--; // 判断缩减容量 return result; } // const 定义为常函数 int getLength() const { // realSize = realSize - 1; 这样会报错 不能修改函数内部的所有变量 c = 11; // mutable 修饰的变量可以在常函数中修改 return realSize; } bool isEmpty() const { return realSize == 0; } void resize() { int netLength = size * 2; T *p = new T[netLength]; // 拷贝数据 for (int i = 0; i < size; ++i) { *(p + i) = this->arr[i]; } // 释放之前的数组 delete[] this->arr; // 重新赋值 this->arr = p; this->size = netLength; } void toString() { cout << "[ "; for (int i = 0; i < realSize; ++i) { cout << arr[i] << ", "; } cout << " ] " << endl; } private: int size{}; // 容器的大小 int realSize{}; // 真实的数组长度 T *arr; // 这里不能使用数组,因为数组名是arr指针常量,不能对arr重新赋值, 指针是指针变量,而数组名只是一个指针常量 mutable int c = 10; // 可以在常函数中修改的变量 需要使用mutable进行修饰 }; int main() { ArrayList<int> arrayList; arrayList.add(1); arrayList.add(2); arrayList.add(3); arrayList.add(4); arrayList.add(5); arrayList.add(6); for (int i = 0; i < arrayList.getLength(); ++i) { cout << arrayList.get(i) << endl; } return 0; } #endif // CPPDEMO_ARRAYLIST_H
1
2
3
4
5
6
字符串
int main() { // 字符串 string 是C++独有的string是一个对象,内部封装了和C一样的字符串的表现形式 string s1(); string s2("123"); string s3 = "wew"; // string字符串是声明在堆区的 string s4(4, 'k'); // 4个K组成 kkkk string s5("123456", 1, 4); // 从1开始,输出四个字符串:2345 cout << s4 << " " << s5 << endl; s2.append(s3); // 追加123wew s2.append(s3, 1, 2); // ew cout << s2 << endl; // 123wewew string sub = s2.substr(2, 3); // 字符串裁剪 cout << sub << endl; // 3we s4.swap(s5); // 字符串交换,只有引用和地址才会改变外部的值 // c_str 支持C,转换为char * string s = "后端码匠"; // 存储在堆区 方法执行完毕 执行析构函数 从堆区移除 // 一般不会这样使用 const char *s_c = s.c_str(); // 将C++ string转换为支持C的字符串,返回常量指针 指针指向了常量,不能通过指针来修改常量 printf("%s\n", s_c); // 一般开发会使用strcpy拷贝,防止被销毁掉等问题 在FFmpeg是使用的C,所以在使用C++开发时必须要对C的转换 char ss[20]; strcpy(ss, s.c_str()); // 拷贝到一个新的变量中 return 0; }
kkkk 2345
123wewew
3we
后端码匠
加载全部内容