c++学习笔记

字数 8831阅读 64

第一天

一.内联函数(inline)

函数调用的时候需要建立栈内存环境,进行参数传递,并产生程序执行转移,这些工作都需要一些时间开销。有些函数使用频率高,但代码却很短。

c++提供inline函数,减少了函数调用的成本

遇到inline函数时直接采用替换的方式,而不用在栈上创建相应环境


二.宏定义

2.1  #define的概念

#define命令是C语言中的一个宏定义命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。

该命令有两种格式:一种是简单的宏定义,另一种是带参数的宏定义。

参照:

http://blog.chinaunix.net/uid-21372424-id-119797.html

例1#defineN 2+2

void main()

{

inta=N*N;

printf(“%d”,a);

}

在此程序中存在着宏定义命令,宏N代表的字符串是2+2,在程序中有对宏N的使用,一般同学在读该程序时,容易产生的问题是先求解N为2+2=4,然后在程序中计算a时使用乘法,即N*N=4*4=16,其实该题的结果为8,为什么结果有这么大的偏差?

(2) 问题解析:

如1节所述,宏展开是在预处理阶段完成的,这个阶段把替换文本只是看作一个字符串,并不会有任何的计算发生,在展开时是在宏N出现的地方 只是简单地使用串2+2来代替N,并不会增添任何的符号,所以对该程序展开后的结果是a=2+2*2+2,计算后=8,这就是宏替换的实质,如何写程序才能完成结果为16的运算呢?

/*将宏定义写成如下形式*/

#defineN(2+2)

/*这样就可替换成(2+2)*(2+2)=16*/


#define area(x)x*x

/*这在使用中是很容易出现问题的,看如下的程序*/

voidmain()

{

inty=area(2+2);

printf(“%d”,y);

}

按理说给的参数是2+2,所得的结果应该为4*4=16,但是错了,因为该程序的实际结果为8,仍然是没能遵循纯粹的简单替换的规则,又是先计算再替换 了,在这道程序里,2+2即为area宏中的参数,应该由它来替换宏定义中的x,即替换成2+2*2+2=8了。那如果遵循(1)中的解决办法,把2+2 括起来,即把宏体中的x括起来,是否可以呢?#define area(x) (x)*(x),对于area(2+2),替换为(2+2)*(2+2)=16,可以解决,但是对于area(2+2)/area(2+2)又会怎么样呢,有的学生一看到这道题马上给出结果,因为分子分母一样,又错了,还是忘了遵循先替换再计算的规则了,这道题替换后会变为 (2+2)*(2+2)/(2+2)*(2+2)即4*4/4*4按照乘除运算规则,结果为16/4*4=4*4=16,那应该怎么呢?解决方法是在整个宏体上再加一个括号,即#define  area(x) ((x)*(x)),不要觉得这没必要,没有它,是不行的。要想能够真正使用好宏定义,那么在读别人的程序时,一定要记住先将程序中对宏的使用全部替换成它所代表的字符串,不要自作主张地添加任何其他符号,完全展开后再进行相应的计算,就不会写错运行结果。

如果是自己编程使用宏替换,则在使用简单宏定义时,当字符串中不只一个符号时,加上括号表现出优先级,如果是带参数的宏定义,则要给宏体中的每个参数加上括号,并在整个宏体上再加一个括号。看到这里,不禁要问,用宏定义这么麻烦,这么容易出错,可不可以摒弃它, 那让我们来看一下在C语言中用宏定义的好处吧。

常用系统宏

_LINE_/*(两个下划线),对应%d*/ 哪一行

_FILE_/*对应%s*/哪一个文件

_DATE_/*对应%s*/什么日期

_TIME_/*对应%s*/什么时间

_func_调用的哪一个函数

总结

宏定义就是复制,在程序编译的时候会做一个替换,所以每一个参数都要加括号,如果是带参数的表达式作为宏的话,则整个表达式需要加括号

三.带参数的函数

int foo(int i = 5,int j= 6){

return 0;

}

int foo1(int = 4,int =8){}

四.函数重载

重载就是函数的名字相同,但是参数不同。可以实现不同类型的相同的类型

五.函数模板

1  泛型编程

独立于任何特定类型的方式编写代码(c,oo,stl、boost)

模板是泛型编程的基础

1.模板使程序员能够快速建立具有类型安全的类库集合和函数集合,他的实现方便了大规模的软件开发

2.现有的框架大多都使用了模板(MFC)

类模板+函数模板

函数模板

template <类型形式参数>返回类型 FunctionName(形式参数表){

//函数定义体

}

template <typename T> T myFunc(T a ,T b){

if(a>b){ 

return a;

}else{

return b;

}

}

//函数模板

template

T absL(T x) {

returnx <0? -x : x;

}

函数模板与函数重载的区别在于,函数模板在实例化后只能进行相同的操作,而函数重载的话在函数体内可以进行不同的操作。

六.类和对象

1、程序化程序设计

1.1程序 = 算法 + 数据结构

1.2算法第一,数据结构第二

2.面向对象程序设计

2.1 程序 = 对象+ 对象+。。

2.2关键:让每一个对象负责执行一组相关任务

3 面向对象编程开发范式的特性

3.1万物皆对象

3.2程序是一组对象彼此之间在发送消息

3.3 每个对象独有自己的内存占用,可以组装成更大的对象

3.4每个对象都有类型性,特定类型的所有对象可以接收相同的消息

4 概念

类: 类是创建对象的模板和蓝图

对象:对象是类的实例化结果

对象是实实在在的存在,代表世界的某一事物

5 对象的三大关键特性

行为:对象能做什么

状态:对象的属性,行为的结果

标识:对象的唯一身份

类和对象的区别

类是静态的定义

对象是动态的实例

程序操作的是对象而非类

建模得到的是类而不是对象

类和对象的联系

类是对象的抽象

对象的产生离不开类这个模板

类存在的目的是实例化得到对象

定义一个类的步骤

1.定义类名

2.编写类的数据成员代表属性

3.编写类的方法代表行为

类的建模是一个抽象和封装的过程

抽象:去掉不关注的,次要的信息而保留重要的信息

封装:信息打包

具体一点:将数据和行为结合在一个包中,对对象的使用者影藏数据的实现方式

实现封装的关键:不能让类中的方法直接访问其他类的内部数据,只能通过公开的行为间接访问

结构体和类的差别

结构体中的属性都是共有的,而类中的属性默认是私有的,有多种模式可选

结构体的安全性不好

类不仅可以保护数据,还可以提供成员函数操作数据

c++中用类来抽象数据类型(ADT)

class name{

private:

私有的成员

私有的函数

protect:

public:


};


类的.h文件写法,注意防止重复包含头文件的写法

类的.cpp文件,注意你要写这个类的域名,写了才可以直接使用它的属性


Carcar1 ;

car1.setProperty(2,4);

car1.run();


#ifndef car_hpp

#define car_hpp

#include

#include

classCar {

public:

voidrun();

voidstop();

voidsetProperty(intprice,intcarNum);

private:

intprice;

intcarNum;

};

#endif/* car_hpp */




#include"car.hpp"

voidCar:: stop(){

std::cout<<__FILE__<<__LINE__<<__func__<

std::cout<<"stop"<

}

voidCar:: run(){

std::cout<<__FILE__<<__LINE__<<__func__<

std::cout<

}

voidCar::setProperty(intprice,intcarNum)

{

//这些都是对象方法,当对象调用这个方法的时候,这个this指针就是对象的内存地址

this->price= price;

this->carNum= carNum;

//也可以直接用的,但是要区分开才行类属性和零时参数

//price = price ;

//carNum = carNum;

}


封装

类背后隐藏的思想是数据的抽象和封装

信息隐藏,隐藏对象的实现细节,不让外部直接访问到

将数据成员和函数封装到一个单元中,单元以类的形式实现

将数据成员和成员函数包装进类中,加上具体实现的隐藏,共同被称作封装,其结果是一个同时带有特征和行为的数据类型。

定义类,定义其数据成员,成员函数的过程称为封装类

信息隐藏是oop最重要的功能之一,也是访问修饰符的原因

信息隐藏的原因包括:


对模块的任何细节所做的更改不会影响使用该模块的代码

防止用户意外修改数据

使模块易于使用和维护


除非必须公开底层实现细节,否则应该将所有字段指定为private加以封装

使数据成员私有,控制数据访问限制,增强了类的可维护行

隐藏方法的实现细节(方法体),向外部提供公开的接口(方法头),以供安全使用

程序开发人员按角色分为类创建者和客户端(应用)程序员

客户端(应用)程序员目标:收集各种用来实现应用开发的类

类创建者目标:构建类,向应用程序员暴露必须的部分,隐藏其他部分

在任何相互关系中,具有关系所色剂的各方都遵守的边界,创建类就建立了与客户端(应用)程序员之间的关系

封装及其访问控制首要存在的原因:让客户端(应用)程序员无法接触到他们不应该接触到的部分---隐藏细节

封装及访问控制的第二个原因:允许创建者改变类内部的工作方式二不用担心会影响到客户端的(应用)程序员--隔离变化,利于维护

补充:

c++中类的实现可以写在类的里面(定义即实现),也可以写在类的外部,只要加上类的作用域就行。在给成员变量赋值的时候可以使用this指针(代表调用该方法的类对象的地址),也可以直接给属性赋值

this指针是的值就是对象起始地址的值,this指针就是指向调用该方法的对象。可以通过指针访问符号->对对象的属性进行访问,函数的调用this->func。

第二天


1 构造函数与析构函数

int i = 0;

int * p = null;//null就是0

类的对象的初始化只能由类的成员函数来进行

建立对象的同时,自动调用构造函数

类对象的定义涉及到一个类名和一个对象名

由于类的唯一性和对象的多样性,用类名而不是对象名来作为构造函数名是比价合适的

默认构造函数

c++规定,每个类必须有一个构造函数

默认构造函数

只要定义了一个构造函数,c++就不提供默认的构造函数

与变量定义类似,在默认构造函数创建对象时,如果创建的是全局对象或静态对象,则对象的位模式全为0,否则对象是随机的

构造函数负责对象的初始化工作,将数据成员初始化

创建对象时,其类的构造函数确保:在用户操作对象之前,系统保证初始化的进行

建立对象,须有一个有意义的初始值

c++建立和初始化对象的过程专门由类的构造函数来完成

构造函数给对象分配空间和初始化

如果一个类没有专门定义构造函数,那么c++仅创建对象而不做任何初始化

构造方法满足以下语法规则:

1 构造方法与类名相同

2 没有返回值类型

3方法实现主要为字段赋值

放在外部定义的构造函数,其函数名前要加上“类名::”

构造函数另一个特殊之处是他么有返回类型,函数体中也不允许返回值,但可以有无值返回语句“return”

因为构造函数专门用于创建对象和为其初始化,所以他是在定义对象时自动调用的

如果创建一个对象数组

car car[3];//构造函数会被调用5次Car::car()

构造函数的类型

Car(intprice ,intcarNum );

Car::Car(intprice ,intcarNum ):price(price),carNum(carNum){};

构造函数前加一个explisit可以避免隐式类型转换

#include

using namespace std;

class Box

{

public :

Box(int h=10,int w=10,int len=10); //在声明构造函数时指定默认参数

int volume( );

private :

int height;

int width;

int length;

};

Box::Box(int h,int w,int len) //在定义函数时可以不指定默认参数

{

height=h;

width=w;

length=len;

}

int Box::volume( )

{

return (height*width*length);

}

int main( )

{

Box box1; //没有给实参

cout<<"The volume of box1 is "<

Box box2(15); //只给定一个实参

cout<<"The volume of box2 is "<

Box box3(15,30); //只给定2个实参

cout<<"The volume of box3 is "<

Box box4(15,30,20); //给定3个实参

cout<<"The volume of box4 is "<

return 0;

}

析构函数

一个类可能在构造函数里分配资源,这些资源需要在对象不复存在以前被释放

析构函数也是特殊类型的成员,它没有参数,不能随意调用,也不能重载。只是在类对象生周期结束的时候由系统自动调用

析构函数名,就在构造函数名之前加上一个逻辑非~运算符即可,表示逆构造函数。

同样道理,只有在用户没有定义析构函数的时候才会系统默认创建一个构造函数

对象在初始化时被分配内存空间,调用构造函数进行初始化,在对象生命周期结束时,销毁对象,调用析构函数释放对象所占用的空间。对象的生命周期时候从他定义时算起,到到出了对象所在大括号时结束。

实例化对象

{

Carcar3(100,20);

}

对象的析构函数

Car:: ~Car(){

cout<price<<endl;

cout<<"xi gou"<<endl;

};

//对象在释放之前会调用这个函数,你可以在对象释放前做一些相应的操作

标准库类型string

中文的字符编码中GB8030,汉子占两个字节

string类型支持长度可变的字符串

c++标准库负责管理与存储字符串所占用的内存(#include <string> using namespace std;)

string 初始化方法


第三天 

static 静态变量,静态成员变量,静态成员函数

参考地址:http://blog.csdn.net/kerry0071/article/details/25741425/

一 面向过程设计中的static关键字

1 静态全局变量

定义:在全局变量前加上关键字static,该变量就被定义成一个静态全局变量

特点:

1)该变量在全局数据区内分配内存

2)初始化:如果不显示初始化,那么将被隐式初始化为0.(自动变量是随机的,除非是显示初始化)

3)该变量只在本源文件可见,严格讲,应该从定义之处开始到本文件结束

注意:

在一个文件中定义了一个全局变量,在另外一个文件中只需要加一个extern即可访问,而如果加上一个static关键字的话就只能在定义这个变量的文件中才能使用

静态变量都在全局数据区分配内存,包括静态局部变量。对于一个完整的程序,在内存中分布情况如下:

代码区

全局数据区

堆区

栈区

一般在程序中,由new产生的动态数据放在堆区中,函数内部的自动变量存放在栈区自动变量会随着函数的退出而释放空间,静态数据(即使是函数nebula的静态局部变量)也会存放在全局数据区中,全局数据区的数据并不会因为函数的退出而释放空间。

2 静态函数与普通函数不同,静态函数只能在声明他的文件当中可见,不能被其他文件使用

二 面向对象程序设计中的static关键字

1.静态数据成员

在类内数据成员的声明前加上static关键字,该数据成员就是类内的静态数据成员。

对非静态成员数据成员,每个对象都有自己的拷贝,而静态数据成员 被当做是类的成员,无论这个类对象被定义了多少个对象,静态数据成员在程序内都只有一份拷贝,由该类型的所有对象共享访问。即静态成员变量是所有的对象共用的,对该类的多个对象来说,静态数据成员只分配一次内存,供所有成员使用。

总结:

说白了,静态数据成员的话就是说,首先它是一个存储在全局区的变量,在程序编译时分配空间,它不依赖于具体存在的对象而存在,它是属于类本身的。因此可以在未实例化对象时,直接通过类名进行调用,对静态成员变量进行访问。当然,这个成员变量是类的数据成员,每一个类的实例化对象都保留了对该对象的拷贝。虽然每个对象都保留了一份数据成员的拷贝,但由于成员变量的访问类型有public,protect,private三种,只有public和protect才可以被类的实例访问到,若将成员变量申明为private的话就只能在类内部访问,无论是类的实例化对象还是类本省都无法访问到private类型对象。只能通过定义接口开可以对静态私有成员变量进行访问。

静态变量的优势:

个人感觉静态变量的优势在于,他只初始化一次,如果是共有的属性的话将被所有的对象所共有,利于节省资源,同时利于对所有对象所共有属性的管理。相比于全局变量的话,静态数据成员具有3种访问属性,可以实现信息的隐藏


静态成员函数

静态成员函数只能存在于类中,属于是为整个类服务和静态成员变量一样。静态成员函数只能访问静态成员变量和静态成员函数。

非静态的成员函数可以访问静态的成员函数和静态成员

调用静态成员函数可以使用成员操作符(.)和(->)为类的对象或指向类对象的指针条调用成员函数

总结:

个人觉得吧,这个静态成员函数也就是一个函数,程序在编译过后会存放在代码区中,类也是代码,所以按道理也会存在于代码区中。通过类实例化对象的过程就是按照类的规则在堆区中开辟相应的空间,成员函数名其实就是函数入口,你是这个类的实例对象那么你就可以调用这个成员函数(重要类允许你调用)。这也就是为什么一个实例对象可以调用类的静态成员函数和普通成员函数的原因。


堆区,栈区,全局区,文字常量区,程序代码区

1 栈区(stack)

由编译器自动分配释放,存放函数的参数值,局部变量的值等。他的地址是从高到低逆序增长

2 堆区(heap)

一般由程序员自己分配释放,若程序员不释放,程序结束时可能由os回收。

3 全局区(静态区)(static)

一,全局变量和静态变量的存放是放在一块儿的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和为初始化的静态变量在相邻的另一块区域,全局变量和静态变量在程序结束后由系统释放

4 文字常量区 

常量字符串就是放在这里的,程序结束后由系统释放

5 代码区 

存放函数体的二进制代码


第三天

动态内存分配


c 语言

malloc/free

c++

new/delete

c++的运算符new/delete

在堆上生成对象,需要调用构造函数

在堆上生成的对象,在释放时需要自动调用析构函数

new/delete,malloc/free需要配对使用

new[]/delete[]生成和释放对象数组也要配对使用

new/delete是运算符,malloc/free是函数调用


小结:

局部变量是存储在栈上的,有擦操作系统管理局部变量的内存。当变量的生命周期结束后,由操作系统对变量的内存进行释放。而使用new在堆上生成的对象就不一样,堆上的对象必须要靠自己手动来释放空间才可以,你要是用了不释放的话,对象将永远占用这部分空间(程序不停止)。在释放了堆空间上的对象空间后,需要将对象指针指空,否则会造成野指针。

代码存放在代码区,数据则根据类型的不同存放在不同的区域中

bss段存放没有初始化或者初始化全为0 的全局变量(没有初始化的全局变量和没有初始化的静态变量在内存中的位置相同)

大多数的操作系统在加载程序的时候会把bss全局变量清零。为保证程序的可移植性,最好把变量的初始值初始化为0;

在程序运行周期内,bss数据一直都存在

data段存放初始化为非0的全局变量

仅仅是将初始化值改为非0,文件的size就变大

同样作为全局变量在整个运行周期内,data数据一直存在

静态成员变量在第一次进入作用域时被初始化,以后不必再初始化

静态成员变量可以在类之间共享数据,也是放在全局/静态数据区中。并且只有一份拷贝


常量数据区(rodata)

rodata存放常量数据

常量不一定要放在常量数据区中,有些立即数和指令编码在一起,放在text中

字符串常量,编译器会去掉重复的字符串,保证只有一个副本

常量是不能修改的

字符串会被编译器自动放到rodata中,加const关键字修饰的全局变量也放在常量数据区中


栈中存储自动名变量或者局部变量,以及传递的参数等

在函数内部定义了一个变量,或者向函数传递参数时,这些变量存储在栈上,当变量退出这些变量的作用域时,这些栈上的存储单元会被自动释放

堆是用户程序控制的存储区,存储动态生成的数据

当用malloc/new来申请一块内存或者创建一个对象是,申请的内存是在堆上分配,需要记录得到的地址,并在不需要的时候释放这些内存

栈内存一般很小,满足不了程序逻辑的要求

对象的生命周期是指对象从创建到被销毁的过程,创建对象要占用一定的内存,因此整个程序占用的内存随着对象的创建和销毁动态地发生变化。

变量的作用域决定了对象的生命周期

全局变量在mian之前被创建,main之后会被销毁

静态对象和全局对象类似,第一次进入作用域被创建,但是程序开始时,内存已经分配好

作用域由{}定义,并不是整个函数

通过new创建对象,很容易造成内存泄露。通过new创建的对象一直存在,知道被delete销毁

隐藏在中间的临时变量的创建和销毁,生命周期很短,容易造成问题。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,具有单个形参,此形参是对该类型的引用。当定义一个新对象并用同一个类型的对象对它进行初始化时,将显示使用拷贝构造函数

当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用拷贝构造函数

如果一个类没有定义拷贝构造函数,编译器会默认提供拷贝构造函数

编译器提供的默认拷贝函数的行为

执行逐个成员初始化,将新对象初始化为原对象的副本

逐个成员指的是编译器将现有对象的每个非static成员依次复制到正在创建的对象

深拷贝和浅拷贝

参考:http://blog.csdn.net/lwbeyond/article/details/6202256/#

未重写拷贝构造函数时

重写了拷贝构造函数时


写的不好



注意:你在构造函数中在堆上申请了一段空间,那么在相应的析构函数中就要释放这段空间


总结:

现在来做一个总结,首先来说类是什么。

类是对客观事物的抽象,是生成具体事物的模板。类就是一段代码。

类的成员变量和成员函数具有private,peotrect,public三种类型。依次对应着不同的访问权限。

通过类实例化一个对象的具体步骤:

1.通过构造函数实例化对象,系统提供默认的构造函数,也可以重写构造函数。在构造方法中可以直接对属性进行赋值,也可以使用初始化列表进行赋值。

2.使用拷贝构造函数对对象进行赋值。同样,系统提供了默认的拷贝构造函数,但系统提供的默认拷贝函数只是简单的进行值拷贝,对于指针类的对象会造成两个对象的同一个指针属性指向同一块内存的问题,最终在调用析构函数释放对象申请的空间时会造成多次同时释放同一块内存的问题,导致程序崩溃。这就是浅拷贝的问题。解决浅拷贝的问题是在拷贝构造函数中对于指针变量指向的空间进行重新分配,而不只是进行简单的赋值。也就实现了对象的深拷贝。

对于用等号进行赋值的对象,其实是对等号运算符进行了重载,并返回自身。

3.等号赋值一般先判断是否是自身赋值给自身,若是自身给自身赋值,则直接返回*this即可。如不是自身给自身赋值,则先看被赋值对象的成员变变量是否是空,如不为空的话先释放成员变量的原有空间,并将指针指空,接着判断赋值对象的相关成员变量是否为空,若不为空则申请相同大小的空间,将相关成员变量的具体值复制即可。最后返回自己*this。本身。

个人觉得搞清楚什么时候调用拷贝构造函数,什么时候调用构造函数,什么时候是运算符重载(等号)非常重要


const关键字

const int buffer = 10;//编译器看得到的

define的话仅仅是替换,属于宏

const指定一个不该被改动的对象

const int * p = &a;//这里的话const修饰的是*p,因此指针所指向的内存区域内的值不可修改

但可以给指针变量p赋新值。

int const *p = &a//这里const修饰的*p,也就说,这是个常量指针,指针的值不可变,但指针所指向的内存空间的值可变

const数据成员必须使用成员初始化列表进行初始化

const成员函数

类接口清晰,确定哪些函数可以修改数据成员




友元函数和友元类

在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时阻止一般访问

友元机制允许一个类将其非公有成员的访问权限授予指定的函数或类

友元的声明以关键字friend开始

只能出现在类定义的内部

可以出现在类中的任何地方,不是授予友元关系的那个类成员,所以不受其声明出现部分的访问控制影响


单例模式

单例模式的好处在于,可以控制生成对象的数量。

他的原理在于,将类的构造函数定义为私有的,也就是说,外界不能调用类的构造函数构造类对象。提供一个静态的成员函数,通过这个静态成员函数返回一个单例对象即可,弹药主语的是你所返回的这个单例模式的对象必须要是类的私有静态对象才行,因为静态成员函数只能访问静态变量,静态成员函数没有this指针,所以不能访问普通成员变量和普通成员函数。另外类的静态成员变量必须要在类的实现中进行赋值,这也从另一方面证明了静态成员变量是属于类本身的说法。

单例可以用来作为数据传递的工具







++,--自增自减运算



自定义string 类型实现

 

重载+= 




其实还是改不了的,因为这是一个常量啊,感觉这就是一个建议而已,为了使得程序更好理解.这里的字符串是只读的


继承

基于已存在的类来构建新类,当从已存在的类继承时,就重用了它的方法和成员,还可以添加新的方法和成员来定制新的类以应对需求

约定:从其他类导出的类叫子类,被导出的类叫父类

is -a继承体现

has-a组合体现

继承的意义:

代码重用

体现不同抽象层次

父子类关系:

父类更抽象,跟一般

子类更具体,更特殊

UML 统一建模语言



类是一种抽象的数据类型,是实例化对象的模板

多继承

多继承的优点是能够获得更多的父类属性,灵活性高

缺点是属性的二义性和数据的冗余

解决方式:

虚继承,即当类执行继承时只保留一份基类的数据

oc里面有个东西叫做协议,用协议的话也可以实现多继承的效果,然而协议更加的高效,只要类与一个协议类型的对象,那么任何一个对象只要实现了协议,都可以为该属性赋值,实现相应的功能。

注意,通过指向基类的指针只能访问派生类中的基类成员,而不能访问派生类心增加的成员。

若想要通过基类的指针访问子类的属性成员需要用到虚函数和指针。

推荐阅读更多精彩内容