提示:本文主要介绍C++中类相关知识及基础概念总结
渺渺何所似,天地一沙鸥
文章目录
- 一、面向对象与面向过程
- 二、类的框架知识
- 2.1 类的定义
- 2.2 类的封装性
- 2.2.1 访问限定符
- 2.2.2 封装的概念以及实现
- 2.3 类的作用域及实例化
- 2.4 类中this指针
- 三、六大默认成员函数
- 3.1 构造函数
- 3.2 拷贝构造函数
- 3.3 析构函数
- 3.4 赋值运算符重载
- 四、类中const成员与static成员
- 五、破坏封装性的友元函数
一、面向对象与面向过程
类是C++一切功能实现的载体,繁华的功能实现都依托于类这颗苍天大树之上。
而C++于C语言最根本的不同就在于C++是面向对象的,C++将一件繁杂的业务拆分成不同的对象,靠一个个精密的对象之间交互完成这个业务,而C语言确实关于问题的过程,分析出求解问题的步骤,通过一个个函数逐步的刨析,调用最终得以解决问题。
二、类的框架知识
2.1 类的定义
在C++中使用关键字class来定义类:
class kind
{
//共有成员
public:
//保护成员
protected:
//私有成员
private:
}
不同于C语言中的struct 在类中可以通过访问限定符的修饰来定义不同权限的函数,这也是C++中一个很大的改变,那什么才是访问限定符呢,它又有什么样的作用?
2.2 类的封装性
2.2.1 访问限定符
C++是一个面向对象的语言,那么他就存在权限的说法,不然人人都可以访问,那和C语言又有什么区别呢?
所以C++的类中引入了三种访问限定符:
public: 成员可以在类的外部通过对象访问。
protected: 成员可以在类的派生类中访问(这个主要在继承体系中使用)因为被它所修饰的成员不能再类外访问,但是可以在类的派生类中通过成员函数来访问基类的保护类成员。
private: 成员只能在类内访问。
而当我们在创建类时,不进行访问限定符的修饰时,默认的访问限定符为private类型,在C++中struct也可以当作class来使用,且struct的默认访问限定符为public类型,这个主要是为了兼容C语言。
所以在C++中我们可以理解为,class与struct在默认访问限定符有区别外,其他方面没有任何区别!
2.2.2 封装的概念以及实现
封装就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,进对外公开接口来和对象进行交互。
当然封装一方面是为了安全性,另一方面也是为了简化我们操作的难度,降低我们看到的代码的冗余性,不然若是暴漏所有的实现细节,那么在文档的查询阅读等方面可能要花费更多的时间与精力,这几乎是不可完成的。
C++中如何实现封装呢
先通过类将对象的属性和方法包装成一个整体,表明包装类实现的是什么功能,然后在内部通过访问限定符,选择性的将接口暴漏给使用者,然后完成交互,比如常见的视频平台会员机制 (普通用户与VIP的界面都是不同的)
#include <iostream>
#include <string>
class Car
{
private:
std::string brand;
std::string model;
public:
Car(const std::string& carBrand, const std::string& carmodel) :brand(carBrand), model(carmodel)
{}
void getbrand()
{
std::cout << "车型:" << brand << std::endl;
std::cout << "车类:" << model << std::endl;
}
};
int main()
{
Car mycar("benchi", "365");
mycar.getbrand();
return 0;
}
Car内部的两个私有接口,不可以直接访问到,然后通过类内共有接口访问,输出汽车信息。较为简单,主要为了体现出类的封装性,类内调用。
2.3 类的作用域及实例化
类有自己独立作用域,类的所有成员都在类的作用域中,通俗的讲类的作用域指定类定义在哪个范围内是可见的,一般可以是全局范围、命名空间、函数内部、或者其他类成员函数内部。
全局作用域及其实例化:
// muclass.h内
class MyClass
{
//类功能
}
#include ”MyClass.h“
int main()
{
MyClass myobject; // 在全局作用域内都可以使用
// ...
return 0;
}
这种就是不加以任何的修饰,以.h头文件的方式定义,然后直接在主函数中定义展开使用,是全局的。
命名空间作用域及其实例化:
// test.h文件
namespace Myname
{
class MyClass
{
// 具体的类定义
}
}
// test.cc 文件
#include "test.h"
int main()
{
// 使用命名空间限定符实例化对象
Myname::MyClass class; // 必须展开.h文件
// ... 具体的操作
}
在命名空间内定义的类可以在该命名空间及其子命名空间中访问。
函数作用域内及其实例化:
void Myfunc()
{
class MyClass
{
// 具体的类定义 ...
}
MyClass myobject; // 仅可以在函数内实例化对象
// ... 具体的操作
}
在函数内定义的局部类只能在该函数内部定义
类的成员函数作用域及其实例化:
// 通俗易懂 类的成员函数作用域肯定是被类中访问限定符所修饰的
class MyClass
{
public:
void mytest()
{
// 类内调用类的其他成员变量以及函数
int x = mytest;
Myfunc();
}
private:
int mytest;
void Myfunc();
// ...
}
上边分别展示了四种作用域以及作用域中实例化的方式。
类在我看来就是一类具有公共属性的集合体。是一种集合的对象,使用多种单个对象结合起来,而我们在对类进行具体的实例化时,可以看作同时实例化了一批我们所需要用到的对象,同样会占用空间,同样要受到访问权限的约束,类就像一个巨大的模板,而对象便是组成这个模板的个体,最终变成了一个巨大的个体, 类就是对数据和行为的封装和抽象!
类的实例化中我们要了解到的一些小知识点:
1.一个类的大小如何计算?
2.空类的大小是多少?
3.什么是内存对齐?结构体是如何进行内存对齐的?
4.如何让结构体按照指定的默认对齐数进行对齐?
5.如何知道结构体中某个成员相对于结构体起始位置的偏移量?
2.4 类中this指针
在函数体中所有的成员变量操作,都是通过this指针去访问的。
this指针的特性:
1.this指针不可改变,所以在类类型为 *const 。
2.只能在 “成员函数” 的内部使用。
3.this指针本质上是一个成员函数的形参,在对象调用成员函数时,第一个就是隐藏的this指针参数,将对象的地址作为实参传递给this形参,所以在对象内存中 并不存储 this指针。
4.this指针是 成员函数 第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
#include <iostream>
#include <string>
class Myclass
{
private:
int _x;
int y;
int z;
public:
Myclass(int x, int y,int z)
{
_x = x; // 当命名不冲突时,可以直接使用命名赋值
this->y = y; // 当命名冲突时,可以使用this指针显示的调用赋值
z = z; // 错误的方式,不会成功
}
void print()
{
std::cout << "_x: " << _x << "\ny: " << this->y << z <<std::endl;
}
};
int main()
{
Myclass c(1, 2,3);
c.print();
return 0;
}
可以看到_x,y都成功赋值,但是z却没有,他们都有各自的地址空间,但就是z没有初始化成功。所以当在成员函数中对成员变量进行操作时,可以显示的调用this指针来进行操作。
切记,成员函数调用的时候第一个传递的参数是隐藏的this指针!
三、六大默认成员函数
3.1 构造函数
构造函数是特殊的成员函数,是类中独有的,且构造函数名与类名相同,创建类对象时由编译器自动调用,在对象的生命周期内仅调用一次。
在编写类时,不显式定义构造函数,编译器会提供默认的构造函数,但默认构造函数不接受任何参数,他只是执行一些默认初始化操作。
在函数创建时,构造函数会为成员变量分配内存,设置默认值或者执行一些其他的初始化逻辑。
一个类可以显示的创建多种类的构造函数,他们具有不同的参数列表,称为构造函数的重载,就可以在构造对象时根据不同的参数提供不同的初始化方式。
#include <iostream>
#include <string>
class Person
{
private:
std::string _name;
int _age;
public:
Person() // 无参构造
{
_name = "张三";
_age = 18;
std::cout << "Person()\n";
}
Person(const std::string &name,int age):_name(name),_age(age) // 具体参数,且使用初始化列表构造
{
std::cout << "Person(const std::string &name,int age)" << std::endl;
}
void print()
{
std::cout << "name: " << _name << "Age: " << _age << std::endl;
}
};
int main()
{
Person p1;
p1.print();
Person p2("李四", 19);
p2.print();
return 0;
}
上边就是显示多构造函数,然后构造函数重载,两种方法都可以成功创建对象,切初始化成功。
在构造函数中还有个独有的模块: 初始化列表 可以对类的成员变量进行初始化有基类特定成员必须在初始化列表中初始化
1.常量成员变量(const),因为常量成员变量的值无法在构造函数体内修改
2.引用成员变量,引用成员变量必须在创建对象时引用有效的对象
3.类类型对象(该类没有默认构造函数)
#include <iostream>
#include <string>
class Myclass
{
private:
const int _num;
int& _val;
public:
Myclass(int num, int& val) :_num(num), _val(val) {
// ---
}
};
int main()
{
int val = 5;
// _num只需要传递同类型的常量过去就好,而_val必须传递一个具有实际地址空间的同类型量,因为它实例化的是一个引用,而引用的定义便需要一块实际存在的空间
Myclass s(6, val);
return 0;
}
而其他的非常量非引用的成员变量看个人习惯,若是直接传入的可以直接在初始化列表进行初始化,且建议在初始化列表初始化,成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
注意!:
1.构造函数不能用const修饰
2.构造函数不能是虚函数
3.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能由一个。
3.2 拷贝构造函数
什么是拷贝构造函数?
拷贝构造函数的概念是只有单个形参,且该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
他就好像构造函数的一种重载形式,所以构造函数的特性拷贝构造函数都满足,拷贝构造函数的参数只有一个且必须使用引用传参,使用传值的方式,就会引发无穷递归调用。
他和构造函数一样也是类的默认函数之一,若未显示的定义,系统会自动生成,默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝一般称为浅拷贝,或者值拷贝(只拷贝值过去)。
因为是默认是浅拷贝,那当我们的类中存在指针变量时,两个原本的对象内容都会收到修改,此时就需要我们手动进行深拷贝了。
class Myclass
{
private:
int* _data;
public:
Myclass(int data)
{
_data = new int;
*_data = data;
}
/*Myclass(const Myclass& data)
{
_data = new int;
*_data = *(data._data);
}*/
void print()
{
*_data = 2;
std::cout << "_data: " << *_data << std::endl;
}
};
int main()
{
Myclass s(1);
Myclass ss(s);
s.print();
return 0;
}
当我们运行 void print(); 函数时,仅仅改变s内的data,ss中的也被改变这就是因为他们中私有成员_data指向的是头一块地址。而若是我们实例化拷贝构造函数,就会出现如下图:
当改变s中值时,对ss中值不影响。
// 手动进行类的析构函数
~Myclass()
{
delete _data;
}
同样一段代码,若显示的调用虚构函数对s的内容进行delete此时,ss中信息也就被释放这样会造成内存卸扣已经一些未知的安全问题。
总结: 编译器生成的拷贝构造函数是浅拷贝,将对象中内容原封不动的拷贝到新对象中,若是原对象中涉及到了资源管理,那么新对象和原对象共用的就是同一份资源,在进行赋值或者析构时会造成内存泄露问题或者是程序崩溃。
3.3 析构函数
析构函数与构造函数的功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数的名称与类名相同,前面加上一个波浪号~作为前缀,并且析构函数不接受任何参数,也无返回值,析构函数在以下情况下会被调用:
1.当对象的生命周期结束时,例如对象超出作用域或被显式的删除。
2.当对象作为另一个对象的成员被销毁时。
3.当对象在类上通过new运算符分配时,然后通过delete运算符删除时。
4.若是类中显示的使用new进行资源申请,那么若是不在析构函数中定义释放,就会造成资源泄露。
若是显示的实例了析构函数,那么就应该在对象销毁之前释放所分配的动态内存、关闭打开的文件、释放占用的其他资源等。
同时建议基类的析构函数最好设置成虚函数。
class Base
{
public:
Base()
{
std::cout << "Base constructor" << std::endl;
}
virtual ~Base()
{
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base
{
public:
Derived()
{
std::cout << "Derived constructor" << std::endl;
}
~Derived()
{
std::cout << "Derived destructor" << std::endl;
}
};
int main()
{
Base* s = new Derived;
delete s;
return 0;
}
若是不将基类设置为虚函数那么就会出现以下情况:
在delete基类指针的派生类对象时,并不会堆派生类进行释放,导致内存泄漏。
在手动释放资源时,可以看出基类与派生类的堆资源都被释放。
之前在多态中我们虚函数的定义是函数名一定要相同才有可能构成虚函数,而析构函数是一种例外,虚函数是一种动态多态,是在运行时实现了,而析构函数的虚函数就是其中的一种例外,所以最好将基类的析构函数声明成虚函数。
3.4 赋值运算符重载
在C++中为了代码的可能性,引入了通过operator关键字来支持运算符重载,运算符重载是针对自定义类型的。
通常的写法为:
operator重载运算符(参数列表)
{
然后进行处理
}
赋值运算符也可以当作一种特殊的函数,用于将一个对象的值赋给另一个对象。
注意事项:
1.不能通过连接其他符号来创建系的操作符:例如:operator@
2.重载操作符必须有一个类类型或者枚举类型的操作数
3.用于内置类型的操作符,的含义不能改变,例如:内置的整形+,-;
4.作为类成员的重载函数时,其形参看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参。
常见运算符重载:
1.赋值运算符重载
2.++、–重载
3.输出和输出运算符(>> <<)重载
4.[ ]运算符重载
5.*和->重载
6.()重载
下面是这些运算符重载实例:
#include <iostream>
#include <string>
class student
{
public:
int _chinese;
int _math;
int _english;
int* _data;
std::string _name;
public:
student(int data,int chinese,int math,int english,std::string name):_chinese(chinese),_math(math),_english(english),_name(name)
{
_data = new int;
*_data = data;
}
void operator=(const int tmp)
{
if (this->_chinese != tmp)
{
_chinese = tmp;
}
}
void operator++()
{
this->_english++;
}
void operator--()
{
this->_math--;
}
int* operator*()
{
return this->_data;
}
void print()
{
std::cout << *_data<<_chinese << _math << _english << _name << std::endl;
}
};
int main()
{
student stu(66,10, 15, 5, "张三");
stu.print();
student stu1(66,11, 16, 5, "小徐");
stu1.print();
stu1 = stu._chinese;
stu1._math--;
stu1._english++;
int* m=stu1.operator*();
std::cout << *m << std::endl;
return 0;
}
一部分赋值运算符重载实现案例。
默认赋值运算符重载:
1.如果一个类没有显示定义赋值运算符重载,编译器会生成一个默认的赋值运算符重载。
2.编译器生成的默认赋值运算符重载是按照浅拷贝方式生成的。
3.如果类中涉及到资源管理时,用户必须显示提供赋值运算符重载,否则可能会造成内存泄露或运行时崩溃,用户按照深拷贝方式提供。
四、类中const成员与static成员
在类中成员一般大的可以分为三类:
普通成员变量:
这一般表示该变量是一个常量,并且在编译阶段会进行参数类型检测以及替换,比宏常量更安全,因此可以用其取代宏常量。
const修饰类成员、函数:
首先const变量,const指的是此变量是只读的,不应该被改变。
如果在程序中修改const变量的值,在编译时,编译器将给出错误提示。
而他的不可修改行,就注定了const类变量必须被初始化。
const int val = 10;
const in val;// 编译器报错,未初始化const变量
val=5; // 编译器报错,给只读变量赋值
修饰成员函数时:
// const修饰成员函数的声明
class Myclass
{
public:
// 在类中用const修饰成员函数时,是在函数的后面加上const
void func() const
{
// 具体的实现
}
}
const成员函数的特性:
1.const成员函数可以被const对象调用,但不能被非const对象调用
2.const成员函数可以访问类的成员变量,但不能修改他们
3.const成员函数可以调用其他const成员函数,但不能调用非const成员函数(除非用const_cast进行转换)。
include <iostream>
#include <string>
class Myclass
{
private:
int _val;
public:
Myclass(int val):_val(val){}
// const类型成员函数
int getval() const
{
return _val;
}
// 非const类型成员函数
int setval(int val)
{
_val = val;
}
};
int main()
{
const Myclass m(10);
std::cout << m.getval() << std::endl;
m.setval(20); // 编译失败,因为m是const对象,不能调用非const成员函数
return 0;
}
当const修饰成员函数的返回值时:
#include <iostream>
#include <string>
class Myclass
{
private:
int _val;
public:
Myclass(int val):_val(val){}
public:
// 在类中const int 和 int const 成员函数返回的是一个常量整数值
// const int* 和 int const* 返回的是一个常量整数的指针
// const int& 和 int const& 返回的是一个只读的整数引用
// 这三组的意义都是相同的
const int getval()
{
return _val;
}
const int& getval1()
{
return _val;
}
};
int main()
{
Myclass s(10);
std::cout << "_val: " << s.getval() << std::endl;
std::cout << "_val: " << s.getval1() << std::endl;
return 0;
}
对于常量值和常量对象的声明,使用const关键字的位置可以放在类型之前或之后,效果是一样的。这是因为const关键字修饰的是类型本身,而不是修饰具体的标识符。
static修饰类成员变量、函数:
在类中被static修饰的成员称为静态成员。
被static修饰的成员变量:
static成员变量知识:
1.静态成员变量不能再初始化列表位置初始化,必须在类外进行初始化,在类外初始化时,必须要加类名::,类中知识声明。
2.静态成员变量是类的属性,不属于某个具体的对象,是类所有对象共享的。
3.不存在在具体的对象中,因此不会影响sizeof的大小
4.可以通过对象.静态成员名,也可以通过类名::静态成员变量名方式访问。
5.在程序启动时,就完成了对静态成员变量的初始化工作。
#include <iostream>
#include <string>
class Myclass
{
public:
static int _level;
Myclass(int val):_val(val)
{}
~Myclass()
{}
void print()
{
std::cout << "_level: " << _level << "_level1: " << _level1 << std::endl;
}
private:
int _val;
static int _level1;
};
// 类中共有static变量初始化
int Myclass::_level = 5;
// 类中私有static变量初始化
int Myclass::_level1 = 6;
int main()
{
Myclass s(5);
s.print();
// 大小计算无影响
std::cout << sizeof(s) << std::endl;
// 类名::访问
std::cout << "Myclass::_level: " << Myclass::_level << std::endl;
int m = s._level;
// 对象.访问
std::cout << "s._level: " << m << std::endl;
return 0;
}
从输出可以看出,对类的大小static变量不会占用空间,因为无论是类内还是内外静态变量都是存储在静态数据区的,而普通的成员变量是存储在各个对象的内存中(若是通过new开辟的成员变量那就再堆空间上,其他在栈空间上)。
被static修饰的成员函数:
static修饰成员函数知识:
1.静态成员函数没有this指针
2.静态成员函数中不能直接访问非静态成员变量,因为所有非静态成员变量都是通过this指针访问的。
3.静态成员函数不能调用普通成员函数。
4.静态成员函数不能被this修饰。
5.静态成员函数不能是虚函数。
6.既可以通过对象,也可以通过类名::方式访问。
#include <iostream>
#include <string>
class Myclass
{
public:
Myclass(int val):_val(val)
{}
~Myclass()
{}
void print()
{
std::cout << _val << std::endl;
}
static int add(int a, int b)
{
return a + b + _val1;
// 静态成员函数不能直接访问非静态成员变量。非静态成员变量都通过this指针调用
// return a + b + _val;
// print(); // 静态成员函数不能调用普通成员函数
}
// virtual static int add1(int a, int b)
//{
// // 静态成员函数不可以是虚拟的
//}
private:
int _val;
static int _val1;
};
int Myclass::_val1 = 5;
int main()
{
Myclass s(3);
s.print();
// 通过对象调用静态成员函数
std::cout << "s.add(1,2): " << s.add(1, 2) << std::endl;
// 通过类名调用静态成员函数
std::cout << "Myclass::add(1,2): " << Myclass::add(1, 2) << std::endl;
return 0;
}
五、破坏封装性的友元函数
概念:友元提供了一种突破封装的方式,有时提供遍历,但是友元破坏了三大特性中的封装!
友元函数:友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,单需要在类内部声明。
特性:
1.友元函数可访问类的私有和保护成员,但不是类的成员函数。
2.友元函数不能用const修饰
3.友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4.一个函数可以是多个类的友元函数。
5.友元函数的调用与普通函数的调用和原理相同
// 定义
#include <iostream>
class Myclass
{
private:
int _val;
public:
Myclass(int val):_val(val){}
friend void showval(const Myclass& tmp);
}
void showval(const Myclass& tmp)
{
//具体实现
}
注意:
1.友元关系是单向的,不具交换性。
2.友元关系不能传递。
3.友元关系不能继承。