面向过程关注的是解决问题的过程,抽象为一些函数。
面向对象则关注的是问题设计的对象以及对象之间的方法,抽象为一些类对象的设计和类成员函数和不同类对象之间的函数。
C++是基于面向对象的,但同时C++又兼容C,所以C++同时可以实现面向对象和面向过程的混合编程。
Java是纯面向对象的语言,只有面向对象的特性。
类是一种广义的自定义类型,在C++中可以用struct或者class定义,如以下一个学生类:
struct student
{
char name[20];
int age;
int id;
};
C++兼容C的结构体用法,同时C++的struct也升级了有类的作用,可以直接用类名来定义对象,不必像C一样加上struct才能定义.
int main()
{
struct student s1;
student s2;
}
C++的类和C的结构体也有很多的不同之处,比如C++中的类中可以定义函数(某种数据和方法合并到一起的理念):
struct student
{
//成员变量
char _name[20];
int _age;
int _id;
//Goole成员变量经常在后面加_
//还有前面加m代表member
//成员方法
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
int main()
{
student s1;
s1.Init("路由器", 20, 2111410800);
s1.print();
}
不过在C++
中,定义类更经常用class
关键字,保留struct
主要是为了兼容C
。
所谓类就是一种自定义的类型,里面可以放数据也可以放函数。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
类有两种定义方式:
class student
{
//成员变量
char _name[20];
int _age;
int _id;
//成员变量的命名风格上
//Goole成员变量经常在后面加_
//还有前面加m代表member
//成员方法
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
int main()
{
student s1;
s1.Init("路由器", 20, 2111410800);
s1.print();
}
把struct
改为class
以后,编译出错了,这是为什么呢,这就可以引出类的访问限定符与封装。
封装:第一层含义是数据和方法放到一起,再一层含义就是就是访问权限限定。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
提供了三种访问限定符:public(公有)、protected(保护)、private(公有)。
【访问限定符说明】
struct
和class
定义类时的主要区别.注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
所以之前的学生类可以这样修改:
class student
{
public:
//成员变量
char _name[20];
int _age;
int _id;
//Goole成员变量经常在后面加_
//还有前面加m代表member
//成员方法
void Init(const char* name, int age, int id)
{
strcpy(_name, name);
_age = age;
_id = id;
}
void print()
{
cout << _name << endl;
cout << _age << endl;
cout << _id << endl;
}
};
一般尽量不要用默认限定修饰符,请明确定义访问限定符,明确定义好权限。
问题:C++中struct和class的区别是什么?
答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。 和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是 private。
封装是一种更好的严格管理,不封装是自由发挥。C语言就是一种自由管理,你可以调方法去控制成员数据,也可以直接访问成员数据,并不限制,但是在C++中并不是这样。
如对于一个栈,top可以指向栈顶元素也可以指向栈顶元素的下一个位置,有时候直接访问top这个数据来做一些功能,可能会因为实现不同而犯错,比如有的栈,top标记为当前栈顶元素的下标,有的栈,top标记为当前栈顶元素的下标加1.
C++为了规避这种可能出现的错误,所以只提供公共的接口,用于实现的私有数据可以使用访问限定符封装起来,不给你访问。
C++的封装做到了:
我们写一个栈,试图把一些方法的实现写到类外面,会发现:
这就引出了类的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
void Stack::Init()
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
这样就ok了。
用类类型创建对象的过程,称为类的实例化.如Stack st;
.
对于前面的Stack
类,我们用sizeof
测试一下它的大小:
int main()
{
Stack s;
cout << sizeof(Stack) << endl;
cout << sizeof(s) << endl;
}
结论是只存了成员对象而没有存成员函数,为什么呢?
思考:不同的类对象调成员函数,调用的不都是同一个成员函数嘛,既然如此,从设计上想,我直接把成员函数放到公共代码段不就好了,为什么要放到每一个类里头呢,所以不考虑一些虚函数的特殊情况时,C++类的大小的计算方法如下:
方法:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
仔细想想空类实例化的对象也可以实例化,也可以取地址嘛,如果都没有大小的话那什么东西在站着这个位置呢,取地址难道返回空指针嘛,所以空类有一个字节来唯一标识类,表明这个类实例化的对象存在。
例题:
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数——其实也相当于空类
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{};
sizeof(A1) = 4;
sizeof(A2) = 1;
sizeof(A3) = 1;
class Date
{
void Init(int year)
{
year = year;
}
private:
int year;
};
这里Init函数里的year到底是成员变量还是形参呢?
是形参。因为变量名称寻找存在就近原则,year会先在这个函数域里头找,就找到了形参。
形参和成员变量同名时,我们怎么找到这个成员变量来解决问题呢?
首先可以考虑用作用域解析作用符号::
class Date
{
void Init(int year)
{
Date::year = year;
}
private:
int year;
};
但是最好不要这么写,类成员名和函数参数名相同,会被骂的(逃),还是按照我们规定好的规则来区分成员变量和形参,如下划线代表类数据成员。
下面我们引出this指针,也可以来解决这个问题,先看一段代码:
class Date {
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << '-' << _month << '-' << _year << '-';
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2022, 1, 14);
d2.Init(2022, 1, 15);
d1.Print();
d2.Print();
}
反汇编如下:
我们知道普通成员函数是放在公共代码段的,通过上图也可以验证,都找的同一个函数地址,但是d1和d2的数据是不同的,Print
如果真的没有参数是怎么知道是d1的调用的它还是d2调用的它呢,这就引出了this指针。
每个成员函数有一个隐藏的this指针,C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数this,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来具体写this指针,编译器自动完成。
//所以Print和Init都会被处理如下
void Init(Date* this)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date* this)
{
cout << this->_year << '-' << this->_month << '-' << this->_day;
}
//调用时相当于
d1.Print();
d1.Print(&d1);
this
指针有三点注意点,语法规定:
d1.Print(&d1)
void Print(Date* this)
,this
,即允许在成员函数内部:this->_year
另外,this
指针是一个*const
,即是一个指针常量,不允许修改this的值,不允许修改this指向别的对象,比如this = nullptr;
会报错。
下面看两个小问题:
this
指针存在哪里呢?一般情况下是在栈里头,因为它是成员函数的形参嘛,但是有的编译器会把this指针放到寄存器里头,VS2019下观察反汇编如下:
可以观察到VS2019把this指针的值放到了ecx
寄存器中,而不是像其他变量一样直接push,(push表示压栈,lea表示取地址,VS2019通过寄存器储存this指针的值).
// 1.下面程序能编译通过吗?
// 2.下面程序会崩溃吗?在哪里崩溃
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
void Show()
{
cout<<"Show()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Show();
p->PrintA();
}
编译时不会检查出空指针错误,空指针不是语法错误,空指针是运行时接引用0地址位置的数据才会错误,所以编译是能通过的。
p->Show()
分析:
这一步不会崩溃,p虽然是空指针,但是p调用成员函数Show()
不会发生空指针访问,因为普通的成员函数不会存在类实例化的对象中,成员函数在公共代码区,这里p会被当做实参传给隐藏的this
指针,但是只要不解引用0位置的数据就不会报错。所以Show()函数没有通过this指针访问成员变量,即没有解引用空指针,就不会出任何错误。
p->PrintA()
分析:
PrintA()
被传了this指针后,在执行过程中访问this->_a
,属于空指针解引用,访问了0地址位置的数据,就会报错。
结论是this指针可以是空指针,只要成员函数不要解引用this指针就行。