类与对象(上)
1.面向过程与面向对象初步认识
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
比如洗衣服,
C语言的做法,每个地方都需要写一个对应的函数。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
也比如洗衣服
2.类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现,会发现struct中也可以定义函数。
//c语言中,结构体只能定义变量,栈的其他函数只能在结构体外写
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps)
{
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void StackPush(struct Stack* ps,int x)
{
ps->a[ps->top++]=x;
}
//...
int main()
{
struct Stack st;
StackInit(&st)
StackPush(&st,1);
//..
return 0;
}
C++对struct进行了升级
1.兼容C中struct的所有用法
2,升级成了类(结构体不仅可以写变量,还可以写函数)
//Stack是类名
struct Stack
{
//成员函数
void Init(int n = 10)
{
//由于在一个类域中,所有类域中的变量可以直接使用
a = (int*)malloc(n * sizeof(int));
if (a == NULL)
{
perror("malloc fail");
return;
}
top = 0;
capacity = n;
}
void Push(int x)
{
a[top++] = x;
}
void Pop()
{
assert(!Empty());
--top;
}
bool Empty()
{
return top == 0;
}
//.....
//成员变量
int* a;
int top;
int capacity;
};
int main()
{
//C的还可以用
struct Stack ss;
//C++把这个当成了一个类
//实例化了一个st的对象
Stack st;
//调用类中的函数
st.Init();
st.Push(1);
st.Push(2);
st.Pop();
st.Destory();
return 0;
}
上面结构体的定义,在C++中更喜欢用class来代替。
3.类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
class Date
{
//public和private是访问限定符,下面解释
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
private:
int year;
int month;
int day;
};
- 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
//Stack.h
class Stack
{
public:
void Init(int n=10);
void Push(int x);
void Pop();
bool Empty();
void Destroy();
private:
int* _a;
int _top;
int _capacity;
};
//Stack.cpp
//函数中的变量会先在局部变量找,找不到就去类中找,找不到再去全局变量中找,再再找不到就报错
void Stack::Init(int n)
{
_a = (int*)malloc(n * sizeof(int));
if (_a == NULL)
{
perror("malloc fail");
return;
}
_top = 0;
_capacity = n;
}
void Stack::Push(int x)
{
_a[_top++] = x;
}
void Stack::Pop()
{
assert(!Empty());
--_top;
}
bool Stack::Empty()
{
return _top == 0;
}
void Stack::Destroy()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
//test.cpp
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Destroy();
}
为什么每个函数前面都要加类名::
原因在于,类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。也可以这样想,如果再写一个队列的类,队列也有差不多的函数,那么当我们想看这个函数内部怎么实现的时候,两个Init,Push…就分不清到底是那个类的函数了。
写项目的时候,一般建议选择第二种写法,不然再类中定义函数,就会显得代码很多,不好看。
函数的命名建议
class Date
{
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
private:
int year;
int month;
int day;
};
//修改之后
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
注意最上面这段代码, 初始化函数是不是有些别扭,因为根本分不清是给赋值是类中的变量还是形参;因此C++申请变量一般前面加个_,或者变量后面加个_;
4.类的访问限定符以及封装
4.1访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用.
访问限定符:限定的是类外面的访问,不限制类里面的访问
【访问限定符说明】
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
问:C++中struct和class的区别是什么?
答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序再说.
4.2 封装
问:面向对象的三大特性
答:封装、继承、多态。
这里主要结束的是封装,继承和多态后面再说,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。
比如电脑,我们知道电脑有cpu,显卡,内存等一些硬件把这些东西封装起来,然后这么复杂的一件东西,提供给用户是键盘,usb接口等等,我们操作电脑也不会直接操作那些硬件,而是使用提供给我们的这些东西。这实际上也是一种封装。
5.类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " "<< _gender << " " << _age << endl;
}
6.类的实例化
用类类型创建对象的过程,称为类的实例化
- 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;
做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//报错
Date::_year = 2023;//原因就是只是声明有这个东西,但是没有开空间
Date::Init()//那这个报错的原因呢?其实是因为this这个指针,下面说
}
- 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。
7.类对象模型
7.1计算类对象的大小
class A
{
public:
void Print()
{
cout << "_a" << endl;
}
private:
int _a;
};
上面这个类的大小是多少呢?
分析一下:有一个char类型,还有一个函数,我们再C语言学过计算结构体的大小。因此char很好算,函数大小这么算,因为函数有多条指令,第一条指令就是函数的地址,就是函数的大小,而指针就是地址,在32位下,指针大小为4个字节,再64位下,指针大小为8个字节,假设我们在32位下,那么这个类的大小最终为5字节。
答案和我们预想的不一样。为啥?
我们发现这个1好像我们char类型的大小,没有计算成员函数的大小。那我们的成员函数放那去了?
7.2类对象的存储方式的猜想
1.对象中包含类的各个成员
缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
2.代码只保存一份,在对象中保存存放代码的地址
3.只保存成员变量,成员函数存放在公共的代码段
公共代码区也是常量区或代码段
问题:对于上述三种存储方式,那计算机到底是按照那种方式来存储的?
其实第二种和第三种感觉都还可以,但是计算机最终选择的是第三种,
假设小区里有一个健身房(成员函数),每户家庭(成员变量),然后去健身房健身
第二种就是,不仅告诉你健身房在哪,还给你健身房密码和健身房地图
第三种就是,直接告诉你健身房在哪里,想去健身自己就去了,不需要地图。
类成员函数是编译器自己放的,并且是编译器自己去拿的,不需要我们自己去找那块空间。
计算下列类的大小
// 类中既有成员变量,又有成员函数
class A1 {
public:
void f1() {}
private:
int _a;
};
// 类中仅有成员函数
class A2 {
public:
void f2() {}
};
// 类中什么都没有---空类
class A3
{
};
int main()
{
cout << sizeof(A1) << endl;
cout << sizeof(A2) << endl;
cout << sizeof(A3) << endl;
return 0;
}
注意我们知道类的成员函数放在常量区,即使不太会算,也要猜测A2和A3是一样大小的,
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象
问:
1. 结构体怎么对齐? 为什么要进行内存对齐?
2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景
4. 如何知道结构体中某个成员相对于起始位置的偏移量
关于内存对齐这块知识可以看最详细讲解结构体,枚举,联合,看完就搞懂了结构体部分。
8.this指针
我们定义一个日期类Date
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year <<" " << _month <<" "<< _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2023, 3, 12);
d2.Init(2023, 4, 12);
d1.Print();
d2.Print();
return 0;
}
对于上述类,有这样的一个问题:
成员函数都存在常量区,是同一个函数,定义两个对象,都是使用了同一个函数,编译器是怎么知道我们初始化的是谁,打印的是谁?
我们在看一下栈是如何知道我们用调用的是谁
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps)
{
ps->a = (int*)malloc(10 * sizeof(int));
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->top = 0;
ps->capacity = 10;
}
void StackPush(struct Stack* ps, int x)
{
ps->a[ps->top++] = x;
}
int main()
{
struct Stack st;
StackInit(&st);
StackPush(&st, 1);
return 0;
}
发现没有,在C语言中,我们每次调用栈的函数,都要传st的地址,然后函数就知道我们要使用st。
而C++为了方便,不用我们每次调函数还需要传这个参数,因此引入了this指针解决这个问题。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
class Date
{
public:
void Init(Date* const this,int year,int month,int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
//void Init(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
void Print(Date* const this)
{
cout << this->_year <<" " << this->_month <<" "<< this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
//Init(&d1,2023,3,12)
d1.Init(2023, 3, 12);
//Init(&d2,2023,3,12)
d2.Init(2023, 4, 12);
//Print(&d1);
d1.Print();
//Print(&d2)
d2.Print();
return 0;
}
this指针的定义和传递,都是编译器的活,我们不能去抢,但是我们可以在类里面使用this指针,我们加了编译器就不加了。
8.2this指针的特性
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
Data::Init()//所以知道这里错误的原因了吗
问:
1. this指针存在哪里?
2. this指针可以为空吗?
答:
1.this是一个形参,当成员函数被调用时,才会被定义,因此存在栈帧中。
2.this指针不能为空,如果在内部把this置为nullptr,就会报运行错误,因为空指针不能解引用。所以this指针类型是,类类型 const。
问:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
// 2.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
答:
1.C,p->Print(),成员函数在常量区,直接去常量区去找这个函数,不会解引用,然后把p的传过去,因为p本身就是一个地址了。传过去之后直接打印。
2.B,和上面一样,把p传过去,但是传过去之后打印会有解引用。this->_a,对空指针解引用会报运行错误。
感觉作者总结的知识对自己有用的小伙伴,请点赞,评论,收藏哦,让你下次不迷路。将会持续更新。