继承
继承,将客观世界中的一般与特殊的关系模型化的过程。即基类抽象出共同特性,子类表达其差别。
概述:子类继承共性,增加自身特殊性
举例:昆虫-->(有翅类,无翅类)-->(有翅类(飞蛾,苍蝇,...),无翅类(...))
多态性
所谓多态,是指一个名字,有多种语义;或一个相同界面,有多种实现。
举例:刹车-->(鼓式刹车,盘式刹车,...)
C++中的体现:函数重载、虚函数与纯虚函数(这两者的不同实现称为重写或覆盖)
PS:对于函数重载,若函数调用(界面)与哪一个函数体(函数实现)相匹配,是在编译时确定,则称为早期匹配。如果是在运行时动态进行,则称为晚期匹配。一般来说,早期匹配执行速度快,晚期匹配提供灵活性和高度的问题抽象。
一般情况:
晚期匹配:虚函数与纯虚函数(都具有相同的函数界面,即相同的函数原型)
早期匹配:普通函数重载(通过参数列表的差异性确定函数体)
注意:
在类中:重写是子类和父类之间的关系,是垂直关系;重载是同一个类中方法之间的关系,是水平关系。
定义上:重载:是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同。调用的时候根据函数的参数来区别不同的函数;覆盖(也叫重写):是指在派生类中重新对基类中的虚函数(注意是虚函数)重新实现。即函数名和参数都一样,只是函数的实现体不一样。
模板(类属机制)
模板的核心是将类型参数化。
实际中有这样一些程序,从它们的逻辑功能(或算法)看,彼此是相同(如求两个数的大小)的,所不同的主要是处理对象(数据)的类型(如整型、浮点型)。如果提供具有相同逻辑功能的程序正文(保存相同性),然后将数据类型作为参数传递(指出不同性),这就是类属机制的思想,又称为参数化模板。
类中的关键字this
this是一个隐含的指针,不能被显式声明,它只是一个形参,一个局部变量,它在任何一个非静态成员函数里都存在。
作用:调用类成员时,告诉系统发起此调用的对象是谁。如:
string x; //声明一个string类的对象x
string y; //声明一个string类的对象y
x.get_length(); //get_length()函数中的this告诉系统调用此函数的对象是x,不是y或者其它对象
y.get_length(); //get_length()函数中的this告诉系统调用此函数的对象是y,不是x或者其它对象
指针
指针是指其值为内存地址的变量,用这个指针所存放的地址值来指向某个数据变量的存储单元;
ps: 数组名、函数名是一个指针常量(常指针),故不能采用a++格式(int a[]={1,2,3,4}),只能采用a+i的形式代表第i个元素地址
函数原型
形式: 返回类型 函数名(参数表),如
int fun(int a, int b)或者int fun(int,int)
(1)确定函数返回类型
(2)确定函数参数个数以及类型、参数按顺序排列(函数重载的根据)
ps:只要编译能区分参数变元的类型,就可以重载一个函数名。
注意:多个同名函数的原型不能只有返回类型不同,而函数名和参数表完全相同
引用
引用是给对象取一个别名,主要有三个用途
(1)独立引用
格式:Type& another_name = Type_var ; 即对类型Type进行引用 ,引用别名是another_name,被引用的对象为Type_var,如
引用必须初始化,并且一旦初始化不可更改引用对象
//变量引用
int i=0;
//j是对i的引用,即j是i的别名。注意,引用必须初始化,并且一旦初始化不可更改引用对象
int & j=i;
//对j进行操作,就是对i进行操作,故i=6
j=6;
//对指针的引用
int r=0;
int & r1=r;
int* p=&r1; //对r1取地址,取的就是r的地址,故p指向r
int & r2=r1; //r2是r1的引用,r1是r的引用,故r2也是r的引用
int *q;
int & qr=q; //qr是指针q的引用,即别名
int b;
qr=&b; //qr指向b,即q指向b
PS:当被引用是常量,如1时,C++新版本用 const int & r=1(早期版本为int & r=1),这样使得语义更明确
结论:独立引用就是一个存储单元的别名
(2)作为参数传递
C与C++都采用传值call by value方式进行参数传递。当一个函数需要对传入的参数进行修改时,C语言通过将参数明确声明为指针实现该目的,如
//交换两个变量的值,传入连个变量的地址,用指针实现
void swap(int * a,int *b){
int t = *a;
*a = *b;
*b = t;
}
C++中允许C的上一个语法,但通过使用引用参数来实现一种更加清晰的方法,如
//交换两个变量的值,通过传入变量的引用实现
void swap(int & a,int & b){
int t = a;
a = b;
b = t;
}
(3)作为返回类型
若一个函数要求返回引用,则return后面应该是一个引用。除了独立引用外,还可以是:
数组元素、static变量、指针所指向的对象(即*p)、结构的分量、联合的分量
结论:函数返回引用时,返回的必须是全局静态数据区的地址,即该引用的对象必须是全局变量或静态变量
PS:函数返回引用,实际上返回的是某个存储单元,这意味着函数调用甚至可以出现在赋值号的左边,即对返回的引用赋值。如
int & get_int(int* p){
return *p;
}
int main(){
int i=1;
int j;
j = get_int(&i) + 5; //j=i+5=6
get_int(&i) = 10; //函数返回i的引用,故i=10
cout<< i << " "<<j<<endl; //输出将为“10 6”
return 0;
}
指针与引用的区别
(1)指针是一个对象的地址,是对所指对象的间接引用;而引用是一个对象的别名,是对对象的直接引用。修改引用就是对引用的对象的修改。
(2)引用是一个对象的别名,因此只能始终指向在初始化时被指定的对象,相当于指针常量;而指针可以被重新赋值,修改指针来指向另一个对象。
(3)指针可以不初始化,引用必须初始化;
内部函数与外部函数
函数的内外之分是针对函数所在的源文件而言,若函数只能在源文件中被调用,则该函数为内部函数,若能被外部其它源文件调用,则为外部函数
(1)函数本质上都是全局的,定义一个函数的目的是为了被另外的函数调用。故若不加特别声明,函数都属于外部函数。
(2)有些函数不想让除源文件内函数外的其它函数调用,故需要指定某些函数为内部函数,指定的关键字为static,故内部函数也称为静态函数;这样使得在不同的文件中可定义同名的内部函数,在多人分工开发时,往往需要这类机制来避免函数名重复。
格式
内部函数格式: static type fun_name(..),如 static int fun(int a, int b)
外部函数格式:extern type fun_name(..),如 extern int fun(int a, int b) ,其中extern关键字可以省略,故默认情况下不含static关键字的函数都是外部函数
注意:在外部文件调用本文件的外部函数时,需要对此函数做声明,且要包含extern关键字(也可省略),如下
// test1.cpp
int fun(inta,int b){
....
}
//test2.cpp
extern int fun(int a,int b); //调用其它文件的外部函数时要提前声明,并且需要加extern关键字
int main(){
int i=0,j=6;
fun(i,j);
return 0;
}
PS:调用其它文件中的函数的另一个便捷方法是使用头文件的包含
注:函数的准确调用,无二义性的保证是依靠函数原型的声明
函数指针
对于函数来说,在编译时,编译系统为函数代码分配一段存储空间,这段存储空间的起始地址(又称入口地址)称为这个函数的指针
可以定义一个指向函数的指针变量,用来存放某一函数的起始地址,则可通过该指针调用该函数。例如:
(1)函数指针定义格式:函数返回类型 (*指针名)(函数参数列表),如下
int (*p) (int,int);
(2)函数指针调用函数格式: (*p)(参数);如
int fun(int a,int b){...} //示例函数
int (*p) (int,int); //定义函数指针
p=fun; //将函数入口地址赋给函数指针
int i=0,j=8;
(*p)(i,j) ; //调用fun函数
p可指向返回类型为整型,参数为两个整型的函数,函数不唯一,可更换指向的函数,只要符合要求
想要通过函数指针调用函数时,需要先将函数入口地址赋给函数指针,函数的入口地址为函数名,故格式如下
int fun(int a, int b){....}
int fun_1(int m, int n){....}
int (*p) (int,int); //定义指向函数 int (int int)的指针
int i=0,j=9;
p=fun; //fun函数入口地址赋给p,不能包含参数,如 p=fun(i,j),
(*p)(i,j); //通过函数指针p调用fun函数
p=fun_1; //将P指向fun_1函数
(*p)(i,j); //调用fun_1函数
PS:
(a)、*p两侧的括号不能省略,因为()的优先级高于*,若不加括号则 int * p(int,int);就变成了 int * (p(int,int));即变成了一个函数的声明。
(b)、p是指向函数的指针变量,它只能指向函数的入口处,不能指向函数中间的某一条指令,故p+1、*(p+1)不能指向下一条指令,类似的表达式无意义。
(3)函数指针作为函数参数
函数指针作为参数,处理流程和普通变量作为参数的操作一致,如
#include<iostream>
using namespace std;
//示例
int min_int(int a,int b){
return a>b ? b:a;
}
float min_float(float a,float b){
return a>b ? b:a;
}
void fun(int (*p) (int,int),int (*q)(float,float)){
int i=0,j=7;
float m=6,n=4;
(*p)(i,j); //调用p指向的函数
(*q)(m,n); //调用q指向的函数
}
int main(){
int (*p1) (int,int); //声明函数指针
p1 = min_int; //指向函数
float (*p2) (float,float);
p2 = min_float;
fun(p1,p2);
return 0;
}
PS: 函数指针作为参数的场景,一般在程序中有多个函数,要根据不同条件执行不同的函数时,如
//求两数小值
int min(int a,int b){
return a>b ? b:a;
}
//求两数大值
int max(int a,int b){
return a>b ? a:b;
}
int main(){
int (*p) (int,int); //声明函数指针
int flag = 1,result;
int m = 80,n = 69;
cout <<"请输入你的选择,1--求最大值,其他数值代表求最小值"<<endl;
cin >>flag;
if(flag == 1){
p = max;
}else{
p = min;
}
result = (*p)(m,n);
return 0;
}
函数库(静态库与动态库)
库:源代码的二进制文件,根据加载的形式,分静态库和动态库(共享库)
windows下:静态库后缀为lib,动态库后缀为dll
linux下:静态库后缀为a,动态库后缀为so
二者的不同点在于代码被载入的时刻不同。
静态库:静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。(以空间换时间)
动态库:共享库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。(以时间换空间)
PS:可执行程序在执行的时候如何定位共享库文件,
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径,此时就需要系统动态载入器(dynamiclinker/loader),对于elf格式的可执行程序,是由ld-linux.so*来完成的,它先后搜索elf文件的 DT_RPATH段—环境变量LD_LIBRARY_PATH—/etc/ld.so.cache文件列表—/lib/,/usr/lib目录。找到库文件后将其载入内存。
以上部分参考自:书籍《C++语言教程(第三版)》
以下部分转载于:https://www.cnblogs.com/wjcoding/p/10955017.html
浅拷贝和深拷贝
C++ 拷贝构造函数分为浅拷贝和深拷贝两种,浅拷贝和深拷贝主要区别就是复制指针时是否重新创建内存空间。
如果没有创建内存只赋值地址为浅拷贝,创建新内存把值全部拷贝一份就是深拷贝。浅拷贝在类里面有指针成员的情况下只会复制指针的地址,会导致两个成员指针指向同一块内存,这样在要是分别delete释放时就会出现问题,因此需要用深拷贝。
类的默认拷贝构造函数
当创建一个类对象时,都会调用其构造函数初始化对象。当需要将一个对象来对新创建的对象进行初始化时,若用户未自定义拷贝构造函数,系统将会调用默认的拷贝构造函数,该默认拷贝构造函数执行的是一个浅拷贝。因为该拷贝构造函数执行时对两个对象进行的是逐域赋值,如下:
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象,内部实际进行的是 s2.num= s1.num;s2.name = s1.name;
}
system("pause");
return 0;
}
执行结果:调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,这会导致什么问题呢?name指针被分配一次内存,但是程序结束时该内存却被释放了两次,会导致崩溃!
这是由于编译系统在我们没有自己定义拷贝构造函数时,会在拷贝对象时调用默认拷贝构造函数,进行的是浅拷贝!即对指针name拷贝后会出现两个指针指向同一个内存空间。
所以,在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。添加了自己定义拷贝构造函数的例子:
#include <iostream>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student();
~Student();
Student(const Student &s);//自定义拷贝构造函数,const防止对象被改变
};
Student::Student()
{
name = new char(20);
cout << "Student" << endl;
}
Student::~Student()
{
cout << "~Student " << (int)name << endl;
delete name;
name = NULL;
}
Student::Student(const Student &s)
{
name = new char(20); //为新对象重新分配动态内存地址
memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);
}
system("pause");
return 0;
}
执行结果:调用一次构造函数,一次自定义拷贝构造函数,两次析构函数。两个对象的指针成员所指内存不同。
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
PS:
当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:
1.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
2.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。
3.浅拷贝带来上述问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。