0.适合小白,和基础不牢的
1.本文很多代码都是图片,但都是完整能运行的;避免有人复制,而不去亲自去敲代码实验;其实,自己敲出来的掌握更牢,看别人的,始终是别人的。
2.本文比较全面回答了学习C/C++过程一些基本概念的疑问;通过化抽象为具体,不去刻意记忆定义,而是去理解定义,能用自己的话表达出来。
3.一定程度上针对面试准备的,不会粘贴大量生硬死板的定义和一些废话,每个概念都有自己的理解以及对应的代码实例。
4.从归类,从大框架到细枝末节的总结方式呈现
5.最终会出一个框架图搭建知识体系网络(未完)
目录
1.c++面向对象如何理解?与面向过程有何不同?
2.那是不是什么都往面向对象上靠,一切可对象化?
3.与c语言有什么不同?相比较的优点是什么?
1.引用和指针的区别?
引用举例:
引用 Vs 指针
引用应用举例:
2.C++ 标准库、C标准库
0.C标准库(都有哪些头文件)
1.C++编译环境
2.C++标准库组成
3. malloc/free和new/delete差别:
new/delete vs malooc/free
new对象和直接定义对象有何不同?
什么时候要用到new 和maolloc呢 ?
应用举例
构造函数,拷贝构造函数、operator=函数
4.结构体和类?
结构体声明、定义、初始化(这里c 、c++里略有不同,见加粗标红)
类声明(前置声明举例和注意点)、定义、实现
Q:什么时候可以不用include一个文件呢
4.C++三大特性是什么?如何理解?
1.封装:在我看来 最主要的功能就是: 安全:1.隐藏细节 2.代码保护(权限控制)
2.继承:可以联想子承父业
3.多态:我理解为同一信号,不同事物会做不同的反应
纯虚函数理解:
5.静态成员函数、静态成员,静态函数、外部函数(extern)
关于const
应用代码展示:
内部函数 VS 外部函数
extern用法:
应用举例:
至于以上疑问:为什么函数加不加extern不报错,变量就必须加上才编译成功呢?
6.友元函数、友元类
7.运算符重载
应用举例
为什么输入输出运算符重载需要声明为友元函数?
8.模板应用
函数模板定义的一般形式如下:
应用举例:
类模板定义的一般形式如下:
9.STL解析及应用
10.C++设计模式
要理解面向对象,必须先理解面向过程,因为面向对象就是为了解决面向过程编程的一些繁杂和缺点(比如代码量大时容易命名冲突,代码重复等);这里推荐https://www.zhihu.com/question/27468564
以下个人理解,有误请指出:
简言概括就是将 现实一类事物高度抽象化,将其抽象为一个有内容(数据)和动作(函数方法)的“活物”,我们无需亲力亲为去做一件事,而是让这个活物去做。不使用这个活物只能说你抽象出了一个类,使用它就是在面向对象了
站在计算机程序角度来讲,面向过程和面向对象的本质理解为:
1.面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行
2.面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
3.面向对象也可以说是从宏观方面思考问题,而面向过程可以说是从细节处思考问题
- 如果你的工程简单,根本不需要解决分类,命名,代码重复等问题,你就不需要非得面向对象编程
- 面向对象
- 多了个好用的引用
- C中用const修饰的变量不可以用在定义数组时的大小,但是C++用const修饰的变量可以。
- C++管理内存方式是对比C较为安全的new/delete
- 引用的概念
引用不是定义一个变量,而是给一个已有成员起一个别名。类型 &引用变量名 = 以定义的变量名
int a = 10; int &b = a;
- 引用的特点
1.一个变量可以有多个成员
2.引用必须初始化
3.引用只能在初始化的时候引用一次,不能改变在引用其他的变量
- 现象上:
1.指针可以不进行初始化,引用必须初始化(因为要知道到底要给谁起别名)
2.指针的值(即地址)可以可以改变,但引用给某个对象起的别名,不能再用在别处,即引用的值不能被改变
3.指针是一个实体,需要分配内存空间。引用只是变量的别名,不需要分配内存空间。
4.引用访问一个变量是直接访问,而指针访问一个变量是间接访问
(无论何时记住,操作别名就等同操作绑定对象,而直接操作指针是地址,操作指向对象要解引用)
- 其他
5.从理论上来说,对于指针没有级数限制,但是引用只有一级
6.sizeof(指针)的大小是固定的(和编译器位数有关),而sizeof(别名)是绑定对象的大小
7.引用较指针更为安全(说实话举不出来例子,只是觉得他一旦绑定了一个对象,就不会再改变,所以安全?有理解深刻的可以留言)
- 作为参数传递
使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;
而使用一般变量传递函数的参数,当发生函数调用时,需要给 形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效 率和所占空间都好。
#include
#include //strtol头文件 void swap1(int * x,int *y) { int temp = *x; *x = *y; *y = temp; printf("*x:%d *y:%d\n",*x,*y); } void swap2(int &x,int &y) { int temp = x; x = y; y = temp; printf("x:%d y:%d\n",x,y); } int main() { int a1 = 10,a2 = 20; swap1(&a1,&a2); swap2(a1,a2); system("pause"); return 0; }
- 常引用
常引用声明方式:const 类型标识符 &引用名=目标变量名;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。
int a ; const int &ra=a; //ra=1; //错误,不允许通过别名对他进行操作 a=1; //正确 int b = a;//只能拿来用,不能拿来修改 printf("b: %d\n",b);//结果:1
引用型参数应该在能被定义为const的情况下,尽量定义为const 。
- 函数返回引用
类型标识符 &函数名(形参列表及类型说明){函数体}
说明:
(1)以引用返回函数值,定义函数时需要在函数名前加&
(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
#include
#include //strtol头文件 int yinyong0(int x) { int sum = x*x; return sum; } int & yinyong1(int x) { int sum = x*x; return sum; } int main() { //int & yinyong = yinyong0(10);//错误:不能返回一个局部变量的引用 int & yinyong = yinyong1(10);//ok int yinyong1_1 = yinyong1(10);//ok printf("yinyong: %d\n",yinyong); system("pause"); return 0; }
- 引用和多态
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
class A;
class B:public A{……};
B b;
A &Ref = b; // 用派生类对象初始化基类对象的引用Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。
1.C++的标准库不仅仅是C标准库的继承,还扩充了不少库函数。(C标准库中大约对应十几个头文件,而C++中有50多个
2.C标准库有哪些?:
它们分别对应一些头文件:https://www.runoob.com/cprogramming/c-standard-library.html 用法介绍参考手册
https://en.cppreference.com/w/c/header
A、C++标准库不是C++语言标准的一部分,由类库和函数库组成。
B、C++标准库中定义的类和对象都位于std命名空间中。
C、C++标准库的头文件都不带.h后缀。
D、C++标准库涵盖了C库的功能,(为了支持类型安全,做了一定的添加和修改)
总之,C++标准库包含C库库的功能,但他俩不是包含关系。只是有相同功能,又不是给覆盖掉了,看你用谁的头文件了。
new/delete vs malooc/free
new/delete底层是基于malloc/free来实现的
①、malloc/free是C和C++语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
②、对于非内部数据对象来说,只使用malloc是无法完成动态对象要求的,一般在创建对象时需要调用构造函数,对象消亡时,自动的调用析构函数。而malloc free是库函数而不是运算符,不在编译器控制范围之内,不能够自动调用构造函数和析构函数。而NEW在为对象申请分配内存空间时,可以自动调用构造函数,同时也可以完成对对象的初始化。同理,delete也可以自动调用析构函数。而mallloc只是做一件事,只是为变量分配了内存,同理,free也只是释放变量的内存。
③、new可以认为是malloc加构造函数的执行。malloc返回类型为void*,必须强制类型转换对应类型指针;new则直接返回对应类型指针;
④、malloc是从堆上开辟空间,而new是从自由存储区开辟(自由存储区是从C++抽象出来的概念,不仅可以是堆,还可以是静态存储区它为自由存储区。这主要取决于operator new实现细节,取决与它在哪里为对象分配空间)
⑤、malloc对开辟的空间大小有严格指定,需要我们计算申请内存的大小;而new只需要对象名,可以自动计算所申请内存的大小。
⑥、malloc开辟的内存如果太小,想要换一块大一点的,可以调用relloc实现,但是new没有直观的方法来改变。
7.malloc开辟内存时返回内存地址要检查判空,因为若它可能开辟失败会返回NULL;new则不用判断,因为内存分配失败时,它会抛出异常bac_alloc,可以使用异常机制;
8.因为new/delete是操作符,它调用operator new / operator delete,它们可以被重载,在标准库里它有8个重载版本;而malloc/free不可以重载;
C++里面, 创建对象: 声明: ClassName object(初始化参数);在Stack栈里面分配空间,自动释放。 new出来:ClassName object=new ClassNam();在heap堆里面分配空间,要手动释放。 1:声明分配内存是在编译阶段进行的,new分配内存是在运行阶段进行的 2:声明被放在栈中,new被分配在堆中或自由存储区中 3:声明创建数组,在编译阶段是就为他分配内存。 new创建在运行阶段 需要创建时就创建,不需要创建时还可以在运行时选择数组长度
当你不知道要创建多少对象的时候,有new比较好,动态分配。
如:
int *a=new int[20];
记得要delete呀
delete [] a;只要能在栈上创建对象,就在栈上创建;否则的话,如果你不得不需要更长的生命周期,只能选择堆上创建
#include
#include //strtol头文件 #include //cin cout所需头文件 using namespace std; class student{ public : int math; int english; int chinese; int sum_score; int eval_score; char * hobby; static int count; student() {} ~student()//析构函数 { cout<<"destroy!"< chinese = chinese; this->english = english; this->math = math; } friend ostream & operator<<(ostream &os,student & s)//这样ok { os<<"math:"< (摘自别处:)
大多数情况下,执行动态内存分配的的类都在构造函数里用new分配内存,然后在析构函数里用delete释放内存。最初写这个类的时候当然不难做,你会记得最后对在所有构造函数里分配了内存的所有成员使用delete。
然而,这个类经过维护、升级后,情况就会变得困难了,因为对类的代码进行修改的程序员不一定就是最早写这个类的人。而增加一个指针成员意味着几乎都要进行下面的工作:
·在每个构造函数里对指针进行初始化。对于一些构造函数,如果没有内存要分配给指针的话,指针要被初始化为0(即空指针)。
·删除现有的内存,通过赋值操作符分配给指针新的内存。
·在析构函数里删除指针。如果在构造函数里忘了初始化某个指针,或者在赋值操作的过程中忘了处理它,问题会出现得很快,很明显,所以在实践中这两个问题不会那么折磨你。但是,如果在析构函数里没有删除指针,它不会表现出很明显的外部症状。相反,它可能只是表现为一点微小的内存泄露,并且不断增长,最后吞噬了你的地址空间,导致程序夭折。因为这种情况经常不那么引人注意,所以每增加一个指针成员到类里时一定要记清楚。
另外,删除空指针是安全的(因为它什么也没做)。所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete掉他们,而不用担心他们是不是被new过。
当然对本条款的使用也不要绝对。例如,你当然不会用delete去删除一个没有用new来初始化的指针,而且,就象用智能指针对象时不用劳你去删除一样,你也永远不会去删除一个传递给你的指针。换句话说,除非类成员最初用了new,否则是不用在析构函数里用delete的。
说到智能指针,这里介绍一种避免必须删除指针成员的方法,即把这些成员用智能指针对象来代替,比如c++标准库里的auto_ptr。
#include
#include
#include//system头文件 struct wml{
int a;
int b ;
char c ;
};//只声明,未定义
struct wml_clone{
int a;
int b ;
char c ;
}WML;//定义但未初始化,此时的WML已经是一个变量了(这里和C不同,在c里,他仍旧是结构体类型)
typedef struct word_lhh123434{
int a;
int b ;
char c ;
}LHH;//名字太长,起别名(这里的LHH == struct word_lhh123434 ,所以他是一种结构体类型 )struct student{
int a;
int b ;
char c ;
}STU={0};//定义且初始化(如果不加typedef,这里STU已经是一个变量了 不是结构体类型了)struct student1{
int a;
int b ;
char c ;
}STU1={1,2,3};//定义且初始化int main()
{
char ch[3] = {0};//所有值初始化为0
char ch1[3] = {1};//是所有值初始化为1了吗? 不是,只有第一个元素是:要达到此效果:使用memset(ch1,szeiof(ch1),1)struct wml s1 = {-10,4,'b'};//此时定义初始化
struct wml s2;
s2.a = 1;
s2.b = 2;
s2.c = '5';//此时定义初始化
wml s3 = {0};//这里竟然不加struct 也没报错,这里是.cpp文件(这里和C不同,c必须加上)LHH lhh = {0};//(注意没加typedef 所以定义起来很麻烦) 此时就不用加Struct了
//WML wml = {0};这么写是错的,WML是一个变量
system("pause");
return 0;
}
1.类的定义
class/struct 类名 //类头
{数据和方法的定义(可能含有类型成员)}; //类体
- 数据
C++11新标准规定,可以为数据成员提供一个类内初始值。在创建对象时,类内初始值将用于初始化数据成员,没有初始值的成员将被默认初始化。但是需要注意:类内初始值要么放在等号右边,要么放在花括号内,记住不能使用圆括号
- 方法
1.所有成员的声明都必须在类的内部,但是成员函数体的定义则既可以在类的内部也可以在类的外部(需要加上类名和作用域运算符(::))
2.在定义类的方法(成员函数)时,如果不需要在方法中修改类的数据成员,在方法声明和定义的参数列表后都必须同时使用const关键字,表示用户不能再该方法中修改类的数据成员,这样的成员函数称为常量成员函数。(1)常量对象,以及常量对象的引用或指针都只能调用常量成员函数。(2)非常量成员函数可以调用常量成员函数,常量成员函数不能调用非常量成员函数
class p { int a(int a,int b) const; }; int a(int a,int b) const { //不能修改类成员的值 }
3.成员函数也可以被重载,只要满足重载的基本条件(参数数量或类型有区别)即可。
特别的,通过区分成员函数是否是const的,可以对其进行重载。因为非常量版本的函数对于常量对象是不可用的,所以只能在一个常量对象上调用const成员函数。另一方面,虽然可以在常量对象上调用常量或非常量版本的函数,但显然此时非常量版本是一个更好的匹配。所以此时,对象是否是const的决定了应该调用哪个函数
4. 编译器分两步处理类:首先编译成员的声明,处理完所有声明后然后才轮到成员函数体。因此,成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。但是需要注意声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了其声明语句之前类中尚未出现的名字,则编译器将会在类的作用域外继续查找
5.可以在定义类的同时定义定义对象
class p{int a;int b();}p1,p2;
- 前向声明
class 类名;
仅仅声明类而暂时不定义它,这种声明被称为前向声明。在它声明之后定义之前该类是个不完全类型。
不完全类型,也是个类型,但是这个类型的一些性质(比如包含哪些成员,具有哪些方法)都不知道。所以不能通过这个前向声明的类的指针或者对象去操作自己的成员。
只能在非常有限的情况下使用:可以定义指向这种类型的指针或引用,也可以作为一个已经声明(但没有定义)的函数的参数或返回类型。前置类型声明只能作为指针或者引用,不能定义类的对象,所以也不能调用对象 中的方法(包括构造函数)
- 使用场景(两个类相互需要使用)
假设有两个类A和B,类A要将类B的对象(或者指正)作为自己的成员使用,并且类B将类A的对象(或者指针)作为自己可以访问的数据,那么这个时候要在a.h中include b.h,同时在b.h 中要include a.h,但是相互包含是不可以的,这个时候就要用到类的前向声明了。
类的前向声明是利用了编译器的特性,编译器在编译的过程中只需要知道各个元素的名称和相应的大小就可以。而在c++中每一个类的大小是固定的,这个时候使用前向声明的类就可以通过编译器
- 应用
假设B类中已经包含了a.h,那么在A类中不能再包含b.h,要在A类中前向声明类B,如下
# include "B.h" class A { A(void); ~A(void); B b_; //要包含B.h }; //B.h class A; class B { B(void); ~B(void); void fun(A& a)//只能是指针或引用 { } //前向声明的类不能实例化对象 A* a_; // ok A a1;//错误 };
Q:什么时候可以不用include一个文件呢
1. 当不需要调用类的实现时,包括 constructor,copy constructor,assignment operator,member function,甚至是address-of operator时,就不用#include,只要forward declaration就可以了。2. 当要用到类的上面那些“方法”时,就要 #include
要是用来定义对象,那就报错喽
- auto
C++11将auto用于实现自动类型判断。这要求进行显示初始化,让编译器能够将变量的 类型设置为初始值的类型。
三大特性:继承、封装、多态
其实顺序来说,应该是 : 封装 、继承、多态
- 将现实问题高度抽象为一类事物,构造类的过程就是封装的过程
公共权限 public :成员类 内部可以访问 外部可以访问
保护权限 protected : 内部可以访问 类外不可访问
私有权限 private :内部可以访问 类外不可访问
protected VS private区别主要在继承方面:
protected :父亲的东西 继承人也可以访问
private: 父亲的小秘密,继承人不可以访问的1.struct 和 class 区别?
答:
1.C++中,class和struct的区别:
成员访问权限:class的成员访问权限是private,而struct是public默认的继承方式,class的默认继承方式private,struct是public
2.struct在c和c++之间的区别
c中,struct是用户自定义数据类型,c++中是抽象数据类型。支持成员定义函数
c中的struct没有权限设置的,在c++中给struct添加了权限设置,增加了访问权限
c中的struct只是变量的聚合体,可以封装数据,但是不可以隐藏,不可以定义函数成员,但c++中的
struct可以定义函数成员。也可以有构造和析构函数
- 为什么要有继承?
答:已有的东西不必再创造,避免不必要的重复。减少冗余,提高代码复用
2. 继承的特性是什么?
简单的说,继承就是指一个对象直接使用另一对象的属性和方法
- 那类里面成员属性有私有,保护,共有类型,他们都能被继承了吗?就像是父亲的所有都能被儿子拿去吗?你想?
答:父亲的东西,父亲当然想给的就给,不给的就不给,可以通过写遗嘱让儿子如何来继承。这就是继承方式。
那么子类(也可称之为派生类)继承父类,也有继承方式。共有继承、私有继承、保护继承。一般情况下都是共
有继承
- C++继承的一般语法为:
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
- 总结看来:
也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。
1) 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调 用)
2) 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
3) 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
4). 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好要显示地写出 继承方式。
父亲私有的,不管让你怎么继承,你都不能享用。父亲共有的和保护的,他不想让你享有就一张private遗嘱给你, 啥也别想要。让你享用那就写public或protect遗嘱给你。父亲只想让你享受,那他就只能给你protct遗嘱了。
注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基 类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使 用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。
3.那派生?重载?重写?覆盖?隐藏?与继承什么关系?区别?概念?
答:这些名词你自己觉得都能拿在一起比较吗?先动动脑子吧!
先来说些概念:
- 派生:为什么有派生,就是为了更清楚表达继承关系,我觉得是这样。就像类A,类B继承类A.,这是侧重类B来 说 的,你要说类A被类B继承,也对着,没毛病,容易不清晰;你说类A派生类B多清楚啊。
还有派生能更好表达继承层级关系:子类这种说法只有一层关系,假如说类A派生B,B派生C,这时候B,C只能说 是 A的派生类,而不能统一表达为子类,会有歧义。
- 重载(overload) 重写(override) 覆盖(override) 隐藏(hiding):
为什么要放在一起说,说明他们很类似,容易混淆。
可以看到重写和覆盖英文单词都一样,说明他们是一个概念了
重载的定义为:在同一作用域中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载,返 回值可不同
重写/覆盖的定义:派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函 数重写
隐藏的定义:指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。
隐藏的实质是;在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同名,就隐藏掉。编 译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。隐藏其实就是想要调用外部同名函数就需要用到
化抽象为具体理解法(以下纯属自编,只是为了加强记忆和理解罢了)
说了定义可以先试着去理解,然后总结有什么区别;曾经这些概念我也是学过的,但还是忘了,为什么?没 理解,又不常用,时间长了,怎么能不忘。大脑容易记住的是有联系的东西,而不是碎片,不去死记定义,可以 化抽象为具体的东西。
首先,我们可以理解重载就是换汤不换勺,什么应用场景呢?一个小孩有好多一样的碗,他想喝粥就去盛粥,想 盛米饭 就去盛米饭。所以函数名就是容器,都一样,里面的食物可以随意变;那为什么返回值无所 谓 呢?你 想放不同 的事物,碗里残渣是不同的呀。为什么要同一作用域呢?小孩这些碗他自己同时只能在一个地方用啊。
重写可以理解为勺汤都不换。什么应用场景呢,一个小孩拿着父亲留的一个祖传碗,孩子也和父亲盛 一样的 饭,父 亲是自己吃,孩子倒给狗吃了。
他俩都有一种特性,就是都想用碗这个工具去最终实现与原来这个碗不同的功能。不过重载是用好多相同的碗分 别去扩展原有的功能,拓展的,原有的我都需要用;而重写则是从父亲继承来的已有的功能,我不用,我要重新实现,我不用原来的了。
在C++中,多态性的实现和联编(也称绑定)这一概念有关。一个源程序经过编译、链接,成为可执行文件的过程是把可执行代码联编(或称装配)在一起的过程。其中在运行之前就完成的联编成为静态联编(前期联编);而在程序运行之时才完成的联编叫动态联编(后期联编)。
- 静态联编支持的多态性称为编译时多态性(静态多态性)。在C++中,编译时多态性是通过函数重载和模板实现的。利用函数重载机制,在调用同名函数时,编译系统会根据实参的具体情况确定索要调用的是哪个函数。
- 动态联编所支持的多态性称为运行时多态(动态多态)。在C++中,运行时多态性是通过虚函数来实现的。
动态多态如何实现?其实就是继承+虚函数重写(特殊的继承)
我的印象就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为
那么在继承中要构成多态还需要两个条件:
a. 调用函数的对象必须是指针或者引用。
b. 被调用的函数必须是虚函数,且完成了虚函数的重写。
- 什么是虚函数?
虚函数:在类的成员函数前加virtual关键字,虚听起来就不实在,所以需要去重写
- 什么是虚函数的重写?就是之前讲的重写,只不过虚函数重写它就变成了多态。
虚函数的重写:派生类中有一个跟基类的完全相同的虚函数。“完全相同”是指:函数名、参数、返回值都相同
- 不规范的重写行为
在派生类中重写的成员函数可以不加virtual关键字,也是构成重写,因为继承后基类的虚函数被继承下来,在派 生类中依旧保持虚函数的属性,我们只是重写了它。这是非常不规范的,在平时尽量不要这样使用。
注意:若子类中的函数有virtual修饰,而父类中没有,则会构成函数隐藏。
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理destructor,这也说明基类的析构函数最好写成虚函数。
接口继承与实现继承
- 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
- 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以,如果不实现多态,不要把函数定义成虚函数。
以下是一个简单的例子。
通过一个Func(person & p),他们的调用各自的虚函数重写
这里派生类可以强转成基类,但是默认基类不可以强转成派生类。
为什么将其转换为基类,他还能执行派生类各自的重写虚函数呢?这里有个虚函数表概念。
通过监视窗口我们发现。p1、p2的有一个_vfptr,这个指针我们称它为虚函数表指针。(不含虚函数的类是没有这个指针的,以下哎,a1是普通类,无)一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(虚表)中,下图我们可以清晰的看到。
派生类的虚表生成:
(1)先将基类中的虚表内容拷贝一份到派生类虚表中;
(2)如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;(就是这样实现多态)
(3)派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类的虚表的最后。
- 如何转换为基类后还能调用子类自己重写的虚函数呢?
由图就能知道,p1里放着基类对象person,person里有一个虚函数表,虚函数表里放着就是子类自己重写函数的地址。如果子类不重写,就放的基类里的eat_food()函数地址,这里重写了,所以会覆盖,eat_food()这个函数地址,知道函数地址,就能成功调用啦
- 纯虚函数:在虚函数的后面写上 = 0,则这个函数为纯虚函数
- 包含纯虚函数的类叫做抽象类(也叫接口类)
- 纯虚函数的声明,是在虚函数声明的结尾加
= 0
,没有函数体。在派生类没有重新定义虚函数之前是不能调用的。抽象类不能实例化出对象。派生类继承后也不能实例化出对象。只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更加体现了接口继承。
- 为什么抽象类不能实例化出对象
- 如果只关注编译器里的现象,是下面这样的
虚函数的原理采用 vtable。 类中含有纯虚函数时,其vtable 不完全,有个空位。 即“纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。所以纯虚函数不能实例化
- 如果更远的,与其问他为什么不能实例化对象,不如问他为什么这么规定?这么设计?https://zhuanlan.zhihu.com/p/95406830
- 引入原因/纯虚函数的作用(百度的)
为了方便使用多态特性:相当于制定标准,使程序更加通用化,可重用性提高,让所有实现它或继承自它的子类全部按同一标准来工作,代码更加清晰,方便管理了。
也可以这么去想:
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
预备知识:这里讲一下,我们加载一个运行程序,他的运行时内存映像
可执行目标文件:prog
加载:加载器将可执行目标文件prog的代码和数据从磁盘复制到内存中,然后跳转到程序的第一条指令开始运行;
那么运行时他在内存中是怎么的呢?类似如下图:详细内容可见:深入理解计算机系统:第七章:链接
C语言中采用const修饰变量,功能是对变量声明为只读特性,并保护变量值以防被修改。举例说明如下:
很多人可能很抵触const,尤其修饰一个很复杂的东西时:static const int * a;
在用的时候多一些思考,问一下为什么,而不是记住他怎么用就行了。
const int i = 5;//变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10;则是错误的。 int const i=5;//同样正确。
注意:
1.定义变量的同时,必须初始化。
2.此外,const修饰变量还起到了节约空间的目的,通常编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。
C语言中const修饰指针要特别注意,共有两种形式,
- 一种是用来限定指向空间的值不能修改;
- 另一种是限定指针不可更改。举例说明如下:
int i = 5; int j = 6; int k = 7; const int * p1 = &i; //定义1 限定指向空间的值 int * const p2 =&j; //定义2 限定指针不可更改
注意:
- 在定义1中const限定的是*p1,
即p1指向空间的值不可改变,若改变其指向空间的值如*p1=20,则程序会报错;
但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。
- 在定义2中const限定的是指针p2,
即p2指向空间的值可改变,如*p2=80是没有问题的,程序正常执行;
但p2的值是不可改变的,若改变p2的值如p2=&k,程序将会报错;
int main() { /* //引用的用法: int a = 10; int &b = a; //用b引用a,b相当于a的别名,操作b就等于操作a */ const int a = 10; //int &b = a; //error C2440: “初始化”: 无法从“const int”转换为“int & //不能用普通引用引用常量 const int &b = a;//引用常量得使用常引用 }
https://bbs.csdn.net/topics/310007610
const int& SetPoint(const int& param) const
第一个const:函数的返回值限定为const,即返回值不能被修改。const int a=SetPoint(...) a在此之后便不能被修改。
第二个const:指函数的形参为const类型,函数体内不能被修改.
第三个const:表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
- 修饰参数的const,如 void fun0(const A* a ); void fun1(const A& a);
如形参为const A* a,则不能对传递进来的指针的内容进行改变,保护了原指针所指向的内容;如形参为const A& a,则不能对传递进来的引用对象进行改变,保护了原对象的属性。
[注意]:参数const通常用于参数为指针或引用的情况,且只能修饰输入参数;若输入参数采用“值传递”方式,由于函数将自动产生临时变量用于复制该参数,该参数本就不需要保护,所以不用const修饰。(请多读几遍)
[总结]对于非内部数据类型的输入参数,因该将“值传递”的方式改为“const引用传递”,目的是为了提高效率。例如,将void Func(A a)改为void Func(const A &a)
对于内部数据类型的输入参数,不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x)不应该改为void Func(const int &x)
- 修饰返回值注意点
1. 一般情况下,函数的返回值为某个对象时,如果将其声明为const时,多用于操作符的重载。通常,不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。
原因如下:如果返回值为某个对象为const(const A test = A 实例)或某个对象的引用为const(const A& test = A实例) ,则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到。
2. 如果给采用“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。如:const char * GetString(void); //如下语句将出现编译错误: char *str=GetString(); //正确的用法是: const char *str=GetString();
3. 函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。如:
class A {… A &operate = (const A &other); //赋值函数 } A a,b,c; //a,b,c为A的对象 … a=b=c; //正常 (a=b)=c; //不正常,但是合法
若负值函数的返回值加const修饰,那么该返回值的内容不允许修改,上例中a=b=c依然正确。(a=b)=c就不正确了。
- 类成员函数中const的使用 (注意点)
一般放在函数体后,形如:void fun() const;
任何不会修改数据成员的函数都因该声明为const类型。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。如:class Test { public: void func() const; private: int intValue; }; void Test::func() const { intValue = 100; } //上面的代码中,常量函数func函数内试图去改变数据成员intValue的值,因此将在编译的时候引发异常。
- 使用const的一些建议
1 、要大胆的使用const,这将给你带来无尽的益处,但前提是你必须搞清楚原委;
2 、要避免最一般的赋值操作错误,如将const变量赋值,具体可见思考题;
3、 在参数中使用const应该使用引用或指针,而不是一般的对象实例,原因同上;
4 、const在成员函数中的三种用法(参数、返回值、函数)要很好的使用;
5 、不要轻易的将函数的返回值类型定为const;
6、除了重载操作符外一般不要将返回值类型定为对某个对象的const引用;
[思考1]: 以下的这种赋值方法正确吗?
const A* c=new A();
A* e = c;
[思考2]: 以下的这种赋值方法正确吗?
A* const c = new A();
A* b = c;[思考3]: 这样定义赋值操作符重载函数可以吗?
const A& operator=(const A& a);
[思考题答案]
1 这种方法不正确,因为声明指针的目的是为了对其指向的内容进行改变,而声明的指针e指向的是一个常量,所以不正确;
2 这种方法正确,因为声明指针所指向的内容可变;
3 这种做法不正确;
在const A::operator=(const A& a)中,参数列表中的const的用法正确,而当这样连续赋值的时侯,问题就出现了:
A a,b,c:
(a=b)=c;
因为a.operator=(b)的返回值是对a的const引用,不能再将c赋值给const常量。
对于static成员变量,如果同时是const的,可以在类定义中初始化,否则只能在类定义外部初始化。
非static的const成员变量只能在构造函数的初始化列表中初始化。(ClassName():m_1(1){};)
观察以下一段代码:
const int i=0;
int *p=(int*)&i;
p=100;
通过强制类型转换,将地址赋给变量,再作修改即可以改变const常量值。
当一个源程序由多个源文件组成时,C语言根据函数能否被其它源文件中的函数调用,将函数分为内部函数和外部函数
内部函数:如果在一个源文件中定义的函数,只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用,这种函数称为内部函数。定义内部函数只需在函数类型前再加一个“static”关键字即可
函数或者变量前面加上static修饰符号,以便把函数或者变量在类内或者文件范围内共享,那么我们把这种函数和变量叫静态函数和静态变量
外部函数:在定义函数时,如果没有加关键字“static”,或冠以关键字“extern”,表示此函数是外部函数
- 静态局部变量
(存储在全局数据区(.data or .bss) 局部变量:栈上,malloc的:在堆上),它具有以下特点:
(1)静态局部变量1.在函数内定义 2.生存期为整个源程序、3但是其作用域仍与局部变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
(2)对基本类型的静态局部变量若在说明时未赋以初值,则在运行时赋予0值。而对局部变量不赋初值,则其值是不定的。根据静态局部变量的特点, 可以 看出它是一种生存期为整个源程序的量。虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可继续使用, 而且保存了前次被调用后留下的 值。 因此,当多次调用一个函数且要求在调用之间保留某些变量的值时,可考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成 意外的副作用,因此仍以采用局部静态变量为宜
- 静态全局变量
全局变量(外部变量)的说明之前再冠以static 就构 成了静态的全局变量。 这两者在存储方式上并无不同(都存储在全局数据区(.data or .bss)),预备知识讲过。
这两者的区别虽在于
非静态全局变量: 作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 静态全局变量: 限制了其作用域, 即只在 定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。
把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量 后是改变了它的作用域, 限制了它的使用范围。因此static 这个说明符在不同的地方所起的作用是不同的
以下针对类:
(编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给 this,所以普通成员函数只能在创建对象后通过对象来调用。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数)
- 静态成员变量(个人认为也属于全局静态变量)
1.存储在全局数据区、2.类内声明,只能类外定义(这点很重要,解释请见下文:疑问解答6)、3.所有对象共享,不是哪个对象独有、4.生存周期直到程序结束、5.作用域:只要包含声明该类的头文件,都可使用啊
- 静态成员函数
1.普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员变量, 调用静态成员函数
2.即可通过对象调用静态成员函数,也可通过类名;普通成员函数不能通过类名
为什么引入静态函数?(类外)
静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其他文件可用.
那肯定是不想被其他文件访问,或者不会与其他文件同名函数冲突
为什么引入静态成员函数? (类内)
静态方法的好处就是不用生成类的实例就可以直接调用。static方法修饰的成员不再属于某个对象,而是属于它所在的类。只需要通过其类名就可以访问,不需要再消耗资源反复创建对象。
在类第一次加载的时候,static就已经在内存中了,直到程序结束后,该内存才会释放。如果不是static修饰的成员函数,在使用完之后就会立即被JVM回收
C++ Primer关于静态成员举了个例子,是说一个银行账户类需要一个数据成员来表示当前的利率。在这个类中,我们所希望的是利率与类相关联,而不是与类的每个对象相关联。这个时候就可以用静态成员函数,可以提高效率,而且一旦利率浮动,每个对象也能相应使用新的值。
如果这个方法是作为一个工具来使用的,就声明为static,不需要new一个对象就可以使用
疑问解答?
1. 写程序验证一下啊:当然ok,连子类继承父类,实例化子类,也算作一次
2、3:根据本篇2、3都是正确的。
4:根据本篇部分的说明,我们知道,4是正确的
5:静态变量放在程序的全局数据区,而不是在堆栈中分配,所以不可能导致堆栈溢出,5是错误的。6.《c++primer》里面说在类外定义和初始化是保证static成员变量只被定义一次的好方法,既然都是所有对象共享的了,那就定义一次就够了。
只有静态常量整型数据成员(static const int count = 0;)才可以在类中初始化(声明+定义),可以在类里面初始化,是因为它既然是const的,那程序就不会再去试图初始化了
目的就是为了实现,1个变量可以在不同文件里多次声明,所有文件统一编译,也不出现错误,这就是extern的用处
首先我们必须清楚的是:我们使用外部的函数,变量,需要声明以下。
- 函数的声明和定义格式大不相同,不加extern也能知道这是函数声明,而不是定义。也就不会报重定义的错误了。
(函数声明 :类型 函数名(); 函数定义:类型 函数名(){ } )
- 而变量的就必须加,因为如果不加,声明和定义就格式一样了。那也不知道他是声明还是定义,自然会报错了。所以必须加上extern加以区分。
为了让一个与类无关的函数fun()或者另一个类A能访问类B的私有数据,可以让这个函数、或类A成为类B的友元函数。
友元函数:在类B中,只需在这个函数原型前面加上friend关键字,而不需要在定义中加上friend。在类B外定义。
友元类:在类B中,添加friend class A;那么A就可以访问类B的所有成员
那么,类A的任何成员函数都成为了类B的友元函数了。
友元函数:必须要通过一个类对象才能访问该类的成员,所以说友元函数必须包含一个类对象的传人参数才能实现类成员的访问。
友元类:通过类对象访问
普通函数可以通过类对象访问类的公有成员,不可以访问类的私有成员
友元函数可以通过类对象访问类的所有成员。
友元函数破坏了封装特性,但是引入友元,是现实场景的需求,当两个类不同,但他们之间的关系不一般,就需要用到友元函数;比如一切使用者和被使用者;司机和汽车,飞行员和飞机,女朋友和男朋友等等。
- 友元函数、友元类特点
- 友元函数可访问类的私有成员,但不是类的成员函数
- 友元函数必须一个类对象的传入参数才能实现类成员的访问,但静态成员既可通过对象也可通过(类名::成员名)访问
- 友元函数,友元类不能被继承,他都不是类成员,怎么继承?
- 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
- 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),--(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
- .:成员访问运算符
- .*, ->*:成员指针访问运算符
- :::域运算符
- sizeof:长度运算符
- ?::条件运算符
- #: 预处理符号
格式解释:这里很重要
类内成员函数重载:运算符左边默认传入的是this指向的对象
重载的要求:
1、内置类型的操作符不能被重载
2、不能为内置类型定义其他的操作符
3、操作符重载不能改变操作符的优先级
4、操作数个数不能改变
重载<<运算符
- 如果非得用类成员函数来重载,就得这么写:ostream& operator<<(ostream& os)
那就是下面这个样子:
这样的格式太奇怪了!!!
那怎么办呢,那就不能是成员函数,就不会默认还传一个this对象进来;那么定义成友元函数(为啥要定义友元?:你不想用类成员函数来重载,你还想访问类的所有成员,那你不就得定义成友元函数了)
- 若是定义为友元函数呢?friend ostream& operator<<(ostream& os, const sudent & s);
则第一个参数是该运算符的第一个操作数,第二个参数是类对象,这下顺序就是 输出对象cout<<类对象,见下图:
因为他不影响习惯代码书写!!!
#include
#include //strtol头文件
#include //cin cout所需头文件
using namespace std;
class student{
public :
int math;
int english;
int chinese;
int sum_score;
int eval_score;
static int count;
student()
{}
student(int math,int english,int chinese){
count++;
this->chinese = chinese;
this->english = english;
this->math = math;
}
friend ostream & operator<<(ostream &os,student & s)//这样ok
{
os<<"math:"<>(istream &is,student & s)//输入>>重载
{
is>>s.chinese>>s.english>>s.math;
return is;
}
};
int student::count = 0;//静态变量用之前需初始化
int main()
{
student s1(50,60,80);
student s2(60,70,80);
student s3(99,99,80);
//以下就可以一次性直接输出一个对象里所有成员的值了
cout<>s1;
cout<
模板,模板就是提供一个统一框架,让他通用化,就像是一个模具,我放进去的材料不同,造出来的东西用途也就不一样
函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。
template<类型形式参数表>
返回类型 函数名(形式参数表)
{
… //函数体
}
这里有一个函数,功能是计算标准差,但是有个问题是,这个只能用在int*类型的,如果是long*,double*等哪怎么办。又写一个内容一样的函数,这样一定可以,但太麻烦。像这类问题可以用模板函数解决。
//---------------------------------------------------------------------------
double StandardDeviation(int* a, const size_t& len)
{
double total =0.0;
double Aver = 0.0;
for(size_t i=0;i
下面是修改后的,这样就可以实现通用类型
//---------------------------------------------------------------------------
template
double StandardDeviation(T a, const size_t& len)
{
double total =0.0;
double Aver = 0.0;
for(size_t i=0;i
template class class-name {
.
.
.
}
未完继续。。。
见我的这篇博客 :https://blog.csdn.net/Wmll1234567/article/details/111246257