C++知识点总结 -- 类
类
前言知识点
— struct
和class
的默认权限不同,struct
默认权限 public;class
默认权限是private;
— .
和::
和:
和->
,在此整理一下这些常用符号的区别。
1、A.B则A为对象或者结构体,B为成员变量或者成员函数;
2、A->B则A为指针,->是成员提取,A->B是提取A中的成员B,A只能是指向类、结构、联合的指针;
3、::是作用域运算符,A::B表示作用域A中的名称B,A可以是名字空间、类、结构;
4、:一般用来表示继承;
类 :
类声明:通常数据成员放在私有部分,成员函数放在公有部分。类声明通常在头文件中声明。类实现通常放在同名的cpp文件中实现;
class className
{
private:
data member declarations;
public:
member function prototypes;
};
公共权限: 类内类外都可以访问;
保护权限: 类(子类也)内可以访问,类外不可以;
私有权限: 类内可以访问,但是子类不可以访问,类外访问不到;
(1)this (this 指针指向被调用的成员函数所属的对象的地址)
this指针,本质是一个指针常量,即指针指向不可以修改
任何对类成员的直接访问都被看作this的隐式引用。
std::string isbn() const {
return bookNo;
}
std::string isbn() const {
return this->bookNo;
} // 两者等价
this指针使用:
1当形参和成员变量同名时,可以使用this指针区分;
2在类的非静态成员函数中返回对象本身,可以使用return *this
,另外返回类型必须是引用,不然返回的是副本,不是本身;
(2)在类的外部定义成员函数
类外部定义的成员的名字必须包含它所属的类名。
double Sales_data::avg_price() const {
if (units_sol)
return revenue/units_sols;
else
return 0;
}
(3)构造函数 (自动调用一次)
定义:类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数:
构造函数没有返回类型;void 都不能写
构造函数可以有参数,可以重载;
构造函数的名字和类名相同。
如果定义了某种构造函数,编译器将不会定义默认的构造函数,需要自己提供。
类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做默认构造函数。
构造函数的分类:
按照参数分类: 无参构造和有参构造
按照类型分类:复制构造函数
: 接收其所属类的对象作为参数 原型:classname(const classname &name);
***当类成员含有地址或指针时,如果使用默认的复制构造函数,则会多个成员指向的参数指向同一个地址,当大于1个对象释放时,会多次释放该成员,从而报错,因此需要提供显示复制构造函数。 简单判断,当类中有使用new初始化的成员,就需要显式话化复制构造函数,用来复制指向的数据,而不是指针,称为深度复制。
如果使用到赋值运算符,在上述情况下,也会有同样的问题,需要显式构造赋值运算符 ----记得在函数内对赋值自身的情况判断;
//example 一个不是很恰当的例子,只为表明为什么要深度赋值
class student
{
private:
string name;
int age;
int *score; //数组保存多个成绩
public:
student(string s, int a,int *scor):name(s), age(a),score(scor){};
void modify(int a,int b);
void print();
student(const student& s);
};
void student::modify(int a, int b){
score[a] = b;
}
void student::print(){
cout<<"name = "<<name<<" ,age = "<<age<<endl;
cout<<"score :";
for(int a = 0; a<2;a++)
{
cout<<" "<<score[a];
}
cout<<endl;
}
student::student(const student &s){
this->age = s.age;
this->name = s.name;
int sc[2] = {s.score[0],s.score[1]};
this->score = sc;
}
int s[2] = {100,100};
student stu1("ssss",12,s);
//当没有显示复制构造函数
student stu2(stu1);//会将stu1中的参数复制给stu2,对name,age数据是正常的,但对score则会使得stu1和stu2指向同一个地址,会出现以下情况,若修改stu1的成绩,stu2也会改变,同样,删除掉stu1,则存在的stu2的score数据会指向异常;
stu2.print();//100 100
stu1.modify(1,60);
stu2.print();//100 60
//显示复制构造函数,则两次打印结果相同
移动构造函数:
接受右值的对象作为函数参数 原型: classname(classname && name){};
将name的元素给该对象,同时对于和地址相关的,记得将name的某些成员指向空;不然就涉及上述的释放问题。 形参一般就是右值,如c1+c2移动赋值构造函数:
上述情况下,也适合使用移动赋值构造函数
classname & classname::operator=(classname && f){
if(this == &f)
return *this;
delete [] pc;
n =f.n;
pc = f.pc;
f.n = 0;
f.pc = nullptr;
return *this
}//其中n为int 型, pc 为 char*
复制构造函数的调用时机:
1将新对象初始化为一个同类对象;
2值传递的方式给函数参数传值;
void dowork(Person p){...}
Person p;
dowork(p); -------------会调用复制构造函数,在把实参按值传递给形参时;
3以值方式返回局部对象;
Person dowork(){
Person p1;
return p1;
}
Person p3 = dowork();
//本来局部对象也会调用复制构造函数,将p1复制给p3;p3和p1地址是不一样的;
默认的方法和禁止的方法:
当我们想使用某个默认的函数,但该函数由于某些原因不会自动创建,则可以使用关键字default
显式声明:如 classname() = default;
当我们想编译器使用特定方法时,可以使用delete
关键字;方法同上;—类似于将该定义放在private;—将不想使用的函数放在private声明,则类外就不能使用到,如不想使用默认的无参构造,则可以把无参构造声明在private内
关键字default只能用于6个特殊成员函数,但delete关键字可以用于任何函数。
delete可以用来禁止特定的转换;
如:类函数: void redo(double);
如果传入5,则会提升为5.0,在使用该方法,若想禁止这种转换,只需要声明: void redo(int) = delete;
构造函数的调用:
1括号法:
默认构造函数调用: Person p1;
** 调用默认构造函数时,不要加括号,因为编译器会认为是一个函数的声明***
有参构造函数: Person p2(10);
复制构造函数: Person p3(p2);
2显示法:
默认构造函数如上;
有参构造函数: Person p2 = Person(10);
复制构造函数: Person p3 = Person(p2);
****Person(10) ; 匿名对象: 当前行执行结束,系统会立即回收匿名对象
****Person(p2)不要利用复制构造函数来初始化匿名对象,编译器会认为 Person(p2) 是Person p2;对象的声明,重定义
3隐式转换法:
有参构造函数:Person p4 = 10; ====== Person p4 = Person (10); -----//仅仅适合一个参数的构造函数
复制构造函数:Person p5 = p4; ======== Person p4 = Person (p4);
初始化列表初始化属性 ------可以解决const成员变量(或者声明为引用的类成员)的赋值问题,如果不采用这种方法给const成员变量赋值,会报错(当
然对于一些情况可以在声明时就赋值,但对于需要赋值来自于用户,就需要使用这种方法。)
----只有构造函数
可以使用这样的方式初始化Person(int i, int j, int k) : a(i), b(j), c(k){}
//就会给Person的三个属性abc赋值ijk;
当类方法不修改成员变量时,尽量将类方法声明成const : void show() const;
::: tip
只有当类没有声明任何构造函数的时,编译器才会自动的生成默认构造函数。一旦我们定义了一些其他的构造函数,除非我们再定义一个默认的构造函数,否则类将没有默认构造函数
委托构造函数:即类的多个构造函数代码可能相同,故可以在一个构造函数类内使用其他构造函数----成员列表初始化的方法;
继承构造函数: 在派生类中使用 using baseclass :: baseclass ;
则派生类就可以使用基类的所有构造函数,但是对派生类中自己的特定版本,会覆盖掉基类的版本。
当然不仅限于构造函数,对基类的其他函数也可以使用该方法。当然继承来的构造方法,只能初始化基类的成员,若对所有成员都想初始化,则使用成员列表初始化语
句。
(4)析构函数
没有返回值,不写void;
函数名同类名,在名称前加~;
析构函数不可以有参数,不可以有参数;
对象在销毁前,会自动调用析构函数,而且只会调用一次;
~Person(){};
×××一定要显式析构函数来释放类构造函数使用new分配的内存,并完成类对象所需的任何特殊的清理工作。对于基类,即使不需要析构函数,也要提供一个虚析构函
数。
(5) 转换函数----比如将类对象转换成int 或者double型operator typeName(); typeName
是要转换的类型
注意: * 转换函数必须是类方法;*转换函数不能指定返回类型;*转换函数不能有参数;
example: 转换为double类型的函数原型 :
operator double();
------p417
静态成员变量: 类内声明(static int A;
),类外初始化(int Person::A = 0;
);----因为声明描述如何分配内存,但不分配内存
静态成员函数: 在函数前加static;
特点:所有对象共享一个函数; 静态成员函数只能访问静态成员变量;
静态成员(变量)函数的调用: 1通过对象;2通过类名Person::A
Person::fname();
常函数const
修饰成员函数 ----常函数: (成员函数后加const
;修饰的是this
指针,让指针指向的值也不可以修改):
特点:1常函数不可以修改成员属性;2成员属性声明时加关键字mutable后,在常函数中可以修改;
常对象:声明对象时前加const:特点:常对象只能调用常函数; 不能修改普通的成员变量
(1)访问控制
| public | 使用public定义的成员,在整个程序内可被访问,public成员定义类的接口。 |
| private | 使用private定义的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。 |
(2)友元
- – - -(友元函数 ,友元类,友元成员函数)
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。
以friend关键字标识。
友元不是类的成员,不受访问控制级别的约束。
友元函数:可以用来访问类的私有部分;
友元的声明仅仅制定了访问的权限,而非通常意义的函数声明。必须在友元之外再专门对函数进行一次声明。
全局函数作友元:
1类中声明(在类的最上方,不用写在public下)friend void goodfriend(...);
2 类外定义void goodfriend(){}
类作友元: 在类中声明(具体位置无所谓,一般在最前面声明):friend class goodgay ;
则goodgay类的所有成员函数就可以访问该类的私有,保护成员了
成员函数作友元:在类中声明:friend void goodgay::visit();
则需要在前面声明好goodgay类和visit方法;
运算符重载
限制条件:
1:重载后至少有一个操作数是用户定义的类型;
2:不能违反原来规则,原来是几元操作符,重载后还必须是几元操作符;
3:不能创建新运算符。
4:sizeof . * :: ?:
等等不能被重载
加法运算符重载: 1成员函数重载;2全局函数重载 总结:对于内置数据类型的表达式的运算符是不能改变的;不要滥用运算符重载
左移运算符重载:<<(只能全局函数重载)
递增运算符重载:
复制运算符重载:
关系运算符重载:
函数调用运算符重载:()—仿函数
class Person
{
friend ostream & operator<<( ostream &cout, Person &p);
friend Person operator+(Person &p1, Person &p2);
public:
ostream & operator<<( ostream &cout, Person &p)
{
cout << "m_A = " << p.m_A << "m_B" << p.m_B;
Return cout;
}
}
继承的重要特点-------基类引用可以指向派生类对象
继承 :(父类中的所有非静态成员属性都会被子类继承、父类的私有成员变量只是访问不到,但是还是继承了)
//example:
class RatedPlayer : public TableTennisPlayer{
...
}
继承中对父类私有成员的处理:
首先,子类的构造函数声明和原来相同
//example
// r用来初始化子类成员变量,其他的是父类成员函数初始化值
//子类构造函数声明
RatedPlayer(unsigned int r =0, const string & fn ="none", const string & ln = "none", bool ht = false);
//等价于
RatedPlayer(unsigned int r, const TableTennisPlayer & tp)
//子类构造函数实现:
RatePlayer::RatePlayer(unsigned int r, const string & fn, const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht){
rating =r;
}
//等价于
RatePlayer::RatePlayer(unsigned int r, TableTennisPlayer & tp) :TableTennisPlayer(fn, ln, ht){
rating =r;
}
这里使用了初始化成员列表的方法,
首先创建基类对象;
派生类构造函数通过成员初始化列表将基类信息传递给基类构造函数;
派生类构造函数应该初始化派生类新增的数据成员。
注意:通常 应该将基类和派生类的声明放在一个头文件里面。(非必须)
基类方法使用总结
1派生类对象自动使用继承来的基类方法,如果派生类没有重新定义这些方法;
2派生类的构造函数自动调用基类的构造方法;
3派生类的构造函数自动调用基类的默认构造函数,如果没有在成员初始化列表中指定其他构造函数;
4派生类构造函数显式调用成员初始化列表中指定的基类构造函数;
5派生类方法可以使用作用域解析运算符来调用公有的和受保护的基类方法;
派生类的友元函数可以通过强制类型转换,将派生类引用或指针转换为基类引用或指针,然后使用该引用或指针来调用基类的友元函数;
派生类和基类关系:
1派生类对象可以使用基类的非私有方法;
2基类指针和引用可以在不进行显式转换的情况下指向派生类对象;—这样就可以在某些参数是基类指针或者引用的函数传递实参时传递派生类或者可以在基类对象调用
基类的赋值构造函数时,传参为派生类;
3基类指针或者引用只能调用基类的方法,不能调用派生类的方法,因为派生类方法中可能用到派生类的新增成员,基类调用就会出错
4,相比于2,不可以将基类对象和地址赋值给派生类引用或指针。
继承方式: 公共继承(子类的权限和父类相同);保护继承(会将父类的公共权限提升到保护权限);私有继承(会将父类的公共权限和保护权限提升到私有权限)
对于父类的私有成员,不管哪种继承都不能访问。对于父类的保护成员,
继承中先调用父类构造函数,在调用子类构造函数,析构顺序相反。
继承同名成员处理方式:
1访问子类同名成员,直接访问即可(如S.m_A
);
2访问父类同名成员,需要加作用域(S.fulei::m_A
);
继承的同名成员函数处理方式:
1访问子类同名成员函数,直接访问即可(如S.func()
);
2访问父类同名成员,需要加作用域(S.Base::func()
);
如果子类中出现了和父类同名的成员函数,子类的同名成员函数会隐藏掉父类的所有·同名成员函数(即函数重载)
继承同名静态成员(可以通过对象(同上)或者通过类名)处理方式:
1访问子类同名静态成员,直接访问(如 Son::A
);
2访问父类静态同名成员,需要加作用域(如Son::Base::A
-----第一个::表示通过类名调用,第二个::表示访问父类作用域下);
继承同名静态成员函数处理方式(通过对象调用同上或者通过类名调用):
1访问子类同名成员函数,直接访问即可(如Son::func()
);
2访问父类同名成员,需要加作用域(Son::Base::func()
);
如果子类中出现了和父类·同名的静态成员函数,子类的同名成员函数会隐藏掉父类的所有·同名成员函数(即函数重载)
多继承中如果多个父类中出现同名情况,子类使用时需要加作用域。
如果子类没有显示的调用父类的构造函数,那么默认会调用父类无参的构造函数!!!
如果父类只提供了有参数的构造函数,那么子类在默认情况下调用父类的无参构造函数时就会报错!
多态 :
静态多态(即函数重载,运算符重载 -----静态多态的函数地址早绑定,编译阶段确定函数地址)和动态多态(派生类和虚函数—动态多态的函数晚绑定,运行阶
段确定函数地址)
虚函数:就是一个类中你希望重载的成员函数,当你用一个基类指针或引用指向一个继承类的对象时,调用虚函数时,实际调用的是继承版本
在函数声明时,最前面加virtual
example: virtual void functionname();
在基类中将方法声明为虚函数,则在派生类中该方法将自动转成虚方法。
虚函数一般是在类继承中,多态时使用,即子类和父类有相同的方法时使用虚函数。
但当派生类提供的版本不同于基类的虚方法时,基类的方法将被隐藏, 当你想覆盖基类的虚方法时,可以在派生类该虚函数处使用override
,若特征标不同,则会
报错;同理,若想禁止覆盖,则可以使用final
。
例如:派生类虚函数:virtual void f(...) const override{...};
当类中存在虚函数时,需要采用动态联编。
动态多态
满足条件:
1存在继承关系
2子类(virtual关键字可不写)需要重写父类的虚函数。
动态多态调用条件
:父类引用或指针指向子类对象。(如函数的形参是父类引用或指针,函数调用是子类对象)
如果使用虚函数,且通过引用或指针而不是对象调用的,程序将根据引用或指针指向的对象的类型来选择方法(动态联编),如果没有使用虚函数,则根据引用或者
指针类型选择方法。
当一个类作为基类时,且存在动态联编,则基类的析构函数应该是虚函数。
A* d = new B();(假定A是基类,B是从A继承而来的派生类)如果析构函数不是虚函数,只会调用父类的析构函数,清除pe中的父类部分指向的内存。
构造函数和友元函数不能是虚函数。
protected和private的区别:在派生类中,派生类成员可以直接访问基类的保护成员(不用通过基类的公有成员方法),但不能直接访问基类的私有成员。对类外的只能使用公有类成员方法来访问保护成员。对派生类而言,基类的保护成员相当于公有成员。
抽象基类(ABC):将两个类或者多个类的共性抽象出来,放在一个ABC中,然后在ABC派生出类。要成为真正的ABC,必须包含一个纯虚函数
好处是可以使用ABC的
指针数组同时管理他的派生类。且抽象基类至少包含一个纯虚函数
纯虚函数:(声明结尾为=0;example:virtual void functionname() cosnt = 0;
)
抽象类–1不能实例化对象,2子类必须重写父类的重虚函数,否则也属于抽象类
包含纯虚函数的只能用作基类,不能实例化对象;
原型中使用=0会指出类是一个抽象基类,在类中可以不定义该函数(当然也可以定义)
继承与动态内存分配:当基类使用类动态内存分配,则本身需要重新定义赋值运算符和复制构造函数和析构函数,派生类如果没有使用动态内存分配,则派生类不需
要重新定义,如果派生类也使用类动态内存分配,则派生类也需要重新定义。
explicit用来防止由构造函数定义的隐式转换。
私有继承:private是默认值,可以省略。example: class Student : private std::string,private std::valarray<double>{}
c++的代码重用:××××包含(将一个类对象包含在另一个类)---------常用××××,私有继承和保护继承
包含:
class Student{
private:
typedef vector<double> ArrayDb
string name;
ArrayDb scores;//使用对象调用来基类方法: scores.size() //
public:
Student(const std::string &a,const ArrayDb &b):name(a),scores(b){};
};
私有继承:—实现has-a的关系 -----尽量少用
class Student:private std::string,private std::valarray<double>{
public:
Student(const std::string &a,const ArrayDb&b):string(a),valarray<double>(b){};
};
//访问基类方法:使用类名和作用域解析运算符来调用基类的方法:ArrayDb::sum() // ArrayDb::size()访问基类对象:私有继承没有name对象,怎麽得到name对象呢?--强制向上类型转换:
const string & Student::Name() const{
return (const string &) *this;
}//该方法返回一个引用,指向调用该方法的Student对象中的继承来的string对象。
保护继承:将私有继承的private换成protected 与私有继承的主要区别在于从派生类派生出另一个类时,私有继承下,第三代类不能使用基类的接口,而保护继承可以。
多重继承:
example :class Waiter : public Work{};
class Singer : public Work{};
class SingWaiter : public Waiter, public Singer{…};
带来的问题
1:SingWaiter 将包含两个Work组件;
解决方法1: SingWaiter ed; Work × pw = &ed 将有二义性; 故 Work × pw1 = (Waiter ×) &ed; Work × pw2 = (Singer ×) &ed;
解决方法2: 虚基类:----复杂
class Waiter : public virtual Work{}; //顺序无关
class Singer : virtual public Work{};
class SingWaiter : public Waiter, public Singer{…};
现在SingWaiter对象只包含Worker对象的一个副本。本质来说,继承的Singer和Waiter对象共享一个Worker。
使用虚基类后,需要改变类构造函数
上一篇: Android动画使用与分析
下一篇: CheckBox自定义样式