一、什么是面向对象 什么是面向过程
??面向过程关注的是解决问题的过程,抽象为一些函数。
??面向对象则关注的是问题设计的对象以及对象之间的方法,抽象为一些类对象的设计和类成员函数和不同类对象之间的函数。
??C++是基于面向对象的,但同时C++又兼容C,所以C++同时可以实现面向对象和面向过程的混合编程。
??Java是纯面向对象的语言,只有面向对象的特性。
二、引入类(对struct的升级)
??类是一种广义的自定义类型,在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;
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 。
三、类的定义
??所谓类就是一种自定义的类型,里面可以放数据也可以放函数。
??类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。
??类有两种定义方式:
- 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理,不过只是可能,具体会怎么处理还是要看编译器。
- 声明放在.h文件中,类的定义放在.cpp文件中,一般推荐第二种定义方法。
class student
{
char _name[20];
int _age;
int _id;
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 以后,编译出错了,这是为什么呢,这就可以引出类的访问限定符与封装。
四、类的访问限定符与封装
??封装:第一层含义是数据和方法放到一起,再一层含义就是就是访问权限限定。
1 访问限定符
??C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
??提供了三种访问限定符:public(公有)、protected(保护)、private(公有)。
【访问限定符说明】
- public修饰的成员在类外可以直接被访问 ;
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的) ;
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止 ;
- class的默认访问权限为private,struct为public(因为struct要兼容C),这也是
struct 和class 定义类时的主要区别.
??注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
??所以之前的学生类可以这样修改:
class student
{
public:
char _name[20];
int _age;
int _id;
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。
2 封装
??封装是一种更好的严格管理,不封装是自由发挥。C语言就是一种自由管理,你可以调方法去控制成员数据,也可以直接访问成员数据,并不限制,但是在C++中并不是这样。
??如对于一个栈,top可以指向栈顶元素也可以指向栈顶元素的下一个位置,有时候直接访问top这个数据来做一些功能,可能会因为实现不同而犯错,比如有的栈,top标记为当前栈顶元素的下标,有的栈,top标记为当前栈顶元素的下标加1.
??C++为了规避这种可能出现的错误,所以只提供公共的接口,用于实现的私有数据可以使用访问限定符封装起来,不给你访问。
??C++的封装做到了:
- 把数据和方法都放到了类里面;
- 可以访问的内容定义成公有,不想暴露的接口定义成私有或保护;
五、类的作用域
??我们写一个栈,试图把一些方法的实现写到类外面,会发现:
??这就引出了类的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。
void Stack::Init()
{
_a = nullptr;
_top = 0;
_capacity = 0;
}
??这样就ok了。
六、对象实例化
??用类类型创建对象的过程,称为类的实例化.如Stack st; .
- 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量 ;
- 类就是宿舍设计图,而对象就是一个个具体的宿舍。
七、类对象模型
1 如何计算类的大小?
??对于前面的Stack 类,我们用sizeof 测试一下它的大小:
int main()
{
Stack s;
cout << sizeof(Stack) << endl;
cout << sizeof(s) << endl;
}
??结论是只存了成员对象而没有存成员函数,为什么呢?
2 类的大小的计算方法
??思考:不同的类对象调成员函数,调用的不都是同一个成员函数嘛,既然如此,从设计上想,我直接把成员函数放到公共代码段不就好了,为什么要放到每一个类里头呢,所以不考虑一些虚函数的特殊情况时,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;
八、this指针
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指针,编译器自动完成。
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 指针有三点注意点,语法规定:
- 我们调用成员函数时不能的显示传实参给this,即不允许
d1.Print(&d1) - 定义成员函数时,也不能显示的写声明形参this,即不允许
void Print(Date* this) , - 不过在成员函数内部,可以显示的使用
this ,即允许在成员函数内部:this->_year
??另外,this 指针是一个*const ,即是一个指针常量,不允许修改this的值,不允许修改this指向别的对象,比如this = nullptr; 会报错。
??下面看两个小问题:
this 指针存在哪里呢?
??一般情况下是在栈里头,因为它是成员函数的形参嘛,但是有的编译器会把this指针放到寄存器里头,VS2019下观察反汇编如下:
??可以观察到VS2019把this指针的值放到了ecx 寄存器中,而不是像其他变量一样直接push,(push表示压栈,lea表示取地址,VS2019通过寄存器储存this指针的值).
- 一道经典面试题:
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指针就行。
|