1.对象的抽象与类的封装
概念:对象:现实中任何事物都可以称之为对象,有自己的独特的特点。
属性是用来描述具体某个对象的特征。例如:小明身高180,体重70千 克,身高和体重就是属性。
面向对象的思想就是把一切事物都看成对象,而对象一般都是由属性和方法组成。
属性属于对象静态的一面,用来形 容对象的一些特性。 方法属于对象动态的一面,
例如,小明会跑,会说话。跑,说话这些行为就是对象的方法! 类:具有同种属性与行为的对象称为类。
一句话总结:类即是对象的抽象,对象是类的实例化。
2.在C++的世界如何封装一个类?
class+类名{
//访问权限
//属性
//访问权限
//行为
};
类的封装=访问权限+成员属性+成员行为
我们把类中变量称为类属性,
类中函数称为成员函数、成员行为、成员方法。
示例代码:
#include <iostream>
using namespace std;
class Stu
{
string name;
int age;
int weight;
public:
void write_code()
{
cout << "正在写代码" << endl;
}
};
类的封装性:就是对类中的属性与方法的访问权限:
public : 公开的,公用的,类外对象或类内均可以直接访问。
private: 私有的,不公开的,只有类内可以访问,类外是不可以访问的。
protected :受保护的,继承类类内可以访问, 类外是不可以访问地。
一般情况下,类的属性设定为prviate, 类中的成员方法都设定为public.
在封装好的类中属性是不被类外读写的,所以我们要设置公共接口
3.访问类中私有属性时,需要手动提供类中的公有的set 、get()方法。
#include <iostream>
using namespace std;
class Stu
{
//定义类中的属性
string name;
int age;
//定义类中方法的访问权限
public:
//定义类中成员属性的访问接口,set、get方法。
void setName(string _name)
{
this->name = _name;
}
string getName()
{
return this->name;
}
void setAge(int _age)
{
this->age = _age;
}
int getAge()
{
return this->age;
}
//定义类中成员方法。
void write_code()
{
cout << "正在写代码" << endl;
}
};
int main()
{
Stu stu;
stu.setName("gaowanxi");
stu.setAge(35);
cout << stu.getName() << endl;
cout << stu.getAge() << endl;
stu.write_code();
return 0;
}
定义一个对象,我们可以直接定义一个类对象,也叫实例化一个对象,也可以通过类指针把对象实例化在堆区。
#include <iostream>
using namespace std;
class Stu
{
string name;
int age;
public:
void setName(string _name)
{
this->name = _name;
}
string getName()
{
return this->name;
}
void setAge(int _age)
{
this->age = _age;
}
int getAge()
{
return this->age;
}
void write_code()
{
cout << "正在写代码" << endl;
}
};
int main()
{
Stu stu;//类对象
stu.setName("gaowanxi");
stu.setAge(35);
cout << stu.getName() << endl;
cout << stu.getAge() << endl;
stu.write_code();
Stu* stu1 = new Stu;//类指针
stu1->setName("zhangsan");
stu1->setAge(18);
cout << stu1->getName() << endl;
cout << stu1->getAge() << endl;
stu1->write_code();
delete stu1;
return 0;
}
4.类内存大小
类对象的内存大小与类中的非静态属性有关。与成员函数无关。
class定义的类遵从C中struct结构体的字节对齐原则。
#include <iostream>
using namespace std;
class Stu
{
private:
//类中静态属性并不占用类或对象的内存对象。
static int age;
static double score;
public:
void showInfo()
{
cout << age << " , " << score << endl;
}
};
//类中静态属性需要在类外进行初始化。
int Stu::age = 18;
double Stu::score = 100;
int main()
{
Stu stu;
cout << sizeof(stu) << endl;
stu.showInfo();
return 0;
}
类内存大小只和成员属性有关。可以自己验证一下。
5.对象初始化和清理
(1)构造函数和析构函数
对象的初始化和清理是两个非常重要的问题,
我们不提供构造和析构,编译器会提供,是空实现,编译器自动调用。
(2)类中构造函数
构造函数的语法形式
1、没有返回值
2、函数名称与类名相同。
3、析构函数可以有参数,所以可以发生重载。
4、自动调用构造,在调用对象时,只调用一次
函数名与类名相同 + 函数形参列表
{
//对类中的属性进行初始化的。
}
(3)类中析构函数
析构函数的语法形式
1、没有返回值
2、函数名称与类名相同,在名称前加~
3、析构函数不可以有参数,所以不可以发生重载。
4、程序在独对象销毁前自动调用析构函数,无需手动且只有一次
~类名()
{
//析构函数的函数体
//析构函数的函数中书写回收为类中有属性指针指堆区资源的逻辑。
}
析构函数的意义:
用来回收类中属性成员属性指针指向堆区的资源的。
(4)构造函数分类及调用
按参数:有参、无参构造
按类型:普通、拷贝构造
三种调用:括号、显示,隐式转换
默认构造函数就是无参构造。
拷贝构造就是用一个已经定义好的类对象,初始化另一个类对象
1、括号法
person p1;//默认构造函数,系统自动分配空间
person p2 ();//有参构造函数
person p3(p2);//拷贝构造函数
*注:调用默认构造是不要加();//编译器认为一个函数声明,不会认为创建对象 person平();这是错误的*
2、显示法
person p1;
person p2= person(10);//有参构造
person p3=perosn(p2);//拷贝构造
person (10);//临时对象 特点:当前行执行结束后,系统会立即回收临时对象
*注:不要用拷贝构造初始化临时对象 person(p2)编译器会认为person (p2)==person p2;*
3、隐式转换
person p4=10;//相当于person p4(10); 有参构造
person p5=p4;//拷贝构造
多敲多练,要知道哪些方式是等价的,尤其是隐式转换。不同场景“=”的语义是不一样的。
#include <iostream>
using namespace std;
class Stu
{
private:
string name;
int age;
public:
//这种没有任何参数的构造,也是编译器默认提供的默认构造。
//这种默认构造,当类中没有定义任何构造时,编译器默认生成的。
//如果,类中有定义任务一个构造,编译器将不再提供默认构造。
//这时,就应该由程序员在定义对象手动指定对象的参数,由编译器自动调用指定的构造,对象类中的属性进行初始化。
Stu()
{
cout << "这是一个Stu的无参的空构造" << endl;
}
Stu(string name, int age)
{
this->name = name;
this->age = age;
cout << "这是Stu的多参的构造" << endl;
}
void showInfo()
{
cout << "姓名:" << this->name << ",年龄:" << this->age << endl;
}
};
int main()
{
//在栈上定义的对象。
Stu stu("gaownxi",35);
stu.showInfo();
return 0;
}
(5)析构函数
语法简单且只有一个
#include <iostream>
using namespace std;
class Stu
{
private:
string name;
int age;
int* p;
public:
Stu()
{
p = new int[1024];
cout << "Stu的无参的构造" << endl;
}
~Stu()
{
//所以说析构函数:就是用回收类中属性指针指向堆区资源的。
delete []p;
cout << "Stu的析构" << endl;
}
};
int main()
{
Stu stu;
Stu* pstu = new Stu;
delete pstu;
return 0;
}
6.类对象和类指针调用析构函数时机
(1)类对象
类对象 顾名思义就是在栈空间定义一个变量,它的内存空间是由系统回收的。
在程序运行完自动调用析构函数,可以在析构函数中打印语句验证。
int mian(){
person p1;
}
(2)类指针
当在堆上定义对象时,使用关键字new, 首先是开辟空间,然后是调用类中与之参数对应的构造函数,构造对象。构造对象过程就是对对象中的属性初始化的过程。
当定义的对象被销毁时,使用关键字delete,或delete[]; 首先调用的类对象的析构函数,然后再释放回空间。
person *p=new person("张三",13,24);
//这个是不会调用析构函数的 必须写delete p;才会调用的
7. 类中特殊属性的初始化
7.1const修饰的类中的属性常成员对象的初始化及没有默认构造的成员对象的初始化。
构造的初始化列表:用来对类中const修饰的特殊属性及类中自定义类类型没有默认构造的情况下指定构造对类中自定义类对象进行构造。
使用初始化列表的方式进行初始化:
#include <iostream>
using namespace std;
class A
{
public:
A(int score)
{
cout << "有参构造" << endl;
}
};
class Stu
{
private:
const int id;
string name;
int age;
int* p;
A a;
public:
//类中的初始化列表的使用方式。
//即对类中的const修饰的属性进行初始化的,同时也可以对类中有自定义类类型却没有默认构造的对象,用来指定调用那一个构造来构造对象的。
//类中的初始化列表的初始化顺序,尽量保持与类中的属性的声明顺序相同。
Stu(int _id,string _name,int _age):id(_id),name(_name),age(_age),p(new int[1024]),a(A(1))
{
cout << "Stu的有参的构造" << endl;
}
~Stu()
{
//所以说析构函数:就是用回收类中属性指针指向堆区资源的。
delete []p;
//书写好习惯
p = nullptr;
cout << "Stu的析构" << endl;
}
void showInfo()
{
cout << "学号:" << this->id << ",姓名:" << this->name << ",年龄:" << this->age << endl;
}
};
int main()
{
Stu stu(1001,"zhangsan",18);
stu.showInfo();
return 0;
}
构造函数的初始化列表,也是由编译器自动调用的,但是他调用的时机在构造函数之前。
即在New之后,构造函数之前。
初始化列表的调用时机:在类开辟空间时,同时会被调用,就是告诉编译器按类中成员的声明顺序进行初始化,构造函数调用前的。
所以初始化列表的特点有以下几点:
1.每个成员变量在初始化列表中,只能初始化一次。
2.类中有特列成员时的初始化:const修饰的对象或变量,引用类型成员或变量,自定义类型指定调用构造函数时使用(特别是有explicit修饰的构造函数的类类型对象时)。
3.如果类中有比较大的结构体对象的初始始化可以入在初始化列表中进行。
4.初始化列表顺序尽量保持与类中成员声明顺序相同。
8.2:.类中static修饰的成员变量的初始化
我们先来看一看static修饰的成员变量具有那些特点:
在C中使用static修饰的成员变量,从内存的角度来看是不是当程序加载时,以32位系统为例,系统会为这个进程分配一个0-4G的内存空间,那么这个内存空间的布局是什么样的呢?
当一个进程被加载时,系统会首先加载静态区数据,静态区的数据就包括了什么呢?
1.文件代码段:编译好的机器码,内容即是一个个函数机器码及保存函数地址的指针。
2.rodata段:即全局只读数据,比如:全局const修饰的全局变量或常量字符串。
3.data段:已初始化的全局变量或static修饰且初始化的变量。(data段数据将在程序执行前系统调用copy_data()函数向程序中拷贝一分副本)
4.bss段:未被初始化的全局变量,或局部定义使用static 修饰的静态变量。(bss段数据在程序执行前系统会调用bss_clear()函数对其进行初始化)
当用static在类中修饰一个成员变量时,由于类中定义的static是隐藏在类中,系统无法调用bss_clear()直接进行初始化。所有C++语法规定,当系统加载时,必须要在类外进static修饰的成员变量进行首次初始化。
如果没有在类外对static修饰的成员变量进行初始化,那么此变量在类中仅为一个声明,而没有定义。当对象引用这个成员变量时就会发生链接错误。
以上带着大家从内存加载的角度分析了static的变量的特点。
所以如果类中的变量在类中有用static修饰,必须要在类外进行初始化。我们一般在main函数前或类的.cpp文件中进行初始化。
#include <iostream>
using namespace std;
class A
{
public:
A(int score)
{
}
};
class Stu
{
private:
const int id;
string name;
int age;
int* p;
A a;
static int count;
public:
//类中的初始化列表的使用方式。
//即对类中的const修饰的属性进行初始化的,同时也可以对类中有自定义类类型却没有默认构造的对象,用来指定调用那一个构造来构造对象的。
//类中的初始化列表的初始化顺序,尽量保持与类中的属性的声明顺序相同。
Stu(int _id,string _name,int _age):id(_id),name(_name),age(_age),p(new int[1024]),a(A(1))
{
cout << "Stu的有参的构造" << endl;
count++;
}
~Stu()
{
//所以说析构函数:就是用回收类中属性指针指向堆区资源的。
delete []p;
cout << "Stu的析构" << endl;
}
void showInfo()
{
cout << "学号:" << this->id << ",姓名:" << this->name << ",年龄:" << this->age << endl;
}
int get_count()
{
//验证静态区数据的份数:
cout << &count << endl;
return count;
}
};
//类外静态成员变量(属性)初始化方式:
int Stu::count = 0;
int main()
{
Stu stu(1001,"zhangsan",18);
stu.showInfo();
cout << stu.get_count() << endl;
Stu stu1(1002,"lisi",20);
cout << stu.get_count() << endl;
return 0;
}
1.由于,静态区数据只会由进程加载一次,在静态区的类成员数据,是所有对象的共享部分。也就是所有对象都共享这份数据,当这分数据被修饰时,所有对象中的这分共享部分都将被修改。
2.由于类中静态成员变量,是属于整个进程的,进程执行前就已经在静态区存在了,而类对象是生存在动态区的,所以此成员不依赖于某个类对象的,他是属于整个进程的,因为他又隐藏于类中,所以我们可以使用类域访问的形式对直接访问。(当于有类内的访问权限的限制。)
3.由于静态成员变量定义在静态区定义内存,而对象是存在于动态区之中,所以静态成员变量并不占用类对象的内存空间。
总结
1.static修饰的成员变量必须在类外进行初始化。
2.static修饰的成员变量不依赖于某个对象,可以直接使用类名 + 域名访问符的形式,直接访问。
3.static修饰的成员变量不占有类对象的空间,且此静态成员数据只有一分,被所有类对象共享。
所以当类中有需要定义一个为这个类而服务,而不是某个对象而服务的属性时,就可以把它升级static的成员属性。
所以当需要一个数据对象为整个类服务,而不是某一个对象服务时,同时又力求不破坏类的封装性,即可以把此成员隐藏在类的内部,对外不可见,同时又可以为每个本类对象共享,即可以把此属性升级的静态属性。
#include <iostream>
#include <unistd.h>
using namespace std;
class A
{
public:
A(int score)
{
}
};
class Stu
{
private:
const int id;
string name;
int age;
int* p;
A a;
static int count;
public:
//类中的初始化列表的使用方式。
//即对类中的const修饰的属性进行初始化的,同时也可以对类中有自定义类类型却没有默认构造的对象,用来指定调用那一个构造来构造对象的。
//类中的初始化列表的初始化顺序,尽量保持与类中的属性的声明顺序相同。
Stu(int _id,string _name,int _age):id(_id),name(_name),age(_age),p(new int[1024]),a(A(1))
{
cout << "Stu的有参的构造" << endl;
count++;
}
~Stu()
{
//所以说析构函数:就是用回收类中属性指针指向堆区资源的。
delete []p;
cout << "Stu的析构" << endl;
}
void showInfo()
{
cout << "学号:" << this->id << ",姓名:" << this->name << ",年龄:" << this->age << endl;
}
int get_count()
{
cout << &count << endl;
return count;
}
};
//类外静态成员变量(属性)初始化方式:
int Stu::count = 0;
//static 在局部空间的使用:static修饰的局部变量只能被初始化一次。
void test()
{
while (true) {
static int i = 0;
i++;
sleep(1);
cout << i << endl;
}
}
int main()
{
Stu stu(1001,"zhangsan",18);
stu.showInfo();
cout << stu.get_count() << endl;
Stu stu1(1002,"lisi",20);
cout << stu.get_count() << endl;
//测试局部作用域的static修饰的变量。
test();
return 0;
}
static在局部作用域 的使用。static修饰的变量只能被初始化一次。
9.this指针
C++中this指针的原型:
使用C风格模拟一下C++中成员函数的调用
#include <stdio.h>
typedef void (*pfun)();
struct Stu
{
char* name;
int age;
pfun f;
};
//这就是this指针的原型
void showInfo(struct Stu* const this)
{
printf("%s \t %d \t",this->name,this->age);
}
int main()
{
struct Stu stu = {"zhangsan",18,showInfo};
stu.f(&stu);
return 0;
}
this指针,即指向本对象的一个常指针。
这也是为什么类中非静态成员函数可以调用类中成员属性的原因。
10.1:this指针的用法:
1.this用来区分成员属性与函数形参变量的区别、
2.用来返回本对象。
#include <iostream>
using namespace std;
class Stu
{
private:
string name;
int age;
public:
Stu(string name, int age)
{
this->name = name;
this->age = age;
}
Stu& setName(string name)
{
this->name = name;
return *this;
}
void showInfo()
{
cout << name << "," << age << endl;
}
};
int main()
{
Stu stu("zhangsan",18);
stu.setName("qiaosi").showInfo();
//stu.showInfo();
return 0;
}
10.常对象和常函数
常对象:就是使用const修饰的类对象
const + 类型 对象名 ; 这个对象就是常对象。
常函数:就是使用const修饰的成员函数。
常函数的const的地方是在函数的函数体前加const就是一个常函数。
class A
{
public:
void showInfo()const
{
//常成员函数的函数体。
}
};
#include <iostream>
using namespace std;
class Stu
{
public:
string name;
int age;
public:
Stu(string name, int age)
{
this->name = name;
this->age = age;
}
Stu& setName(string name1)
{
name = name1;
return *this;
}
void setAge(int age)
{
this->age = age;
}
//常函数是不可以修改类中的属性的。但是是可以有读取类中的属性。
int getAge()const
{
return this->age;
}
void showInfo()
{
cout << name << "," << age << endl;
}
};
bool compair_StuAge(const Stu& stu1, const Stu stu2)
{
return stu1.getAge() > stu2.getAge();
}
int main()
{
Stu stu("zhangsan",18);
stu.setName("qiaosi").showInfo();
//stu.showInfo();
Stu stu1("gaowanxi",35);
bool ok = compair_StuAge(stu,stu1);
if(ok)
{
cout << "qishi大" << endl;
}
else {
cout << "gaowanxi大" << endl;
}
return 0;
}
常对象与常函数在程序设计中的意义:
常对象只能调用常函数,不可以调用普通函数,因为变通函数有修改类中属性的权限。
常函数只能读取类中的成员属性,而不可以修改类中成员函数。
普通对象是可以调用常函数的。
静态成员对象与静态成员函数的意义?
我们知道静态成员对象是定义在静态区的内存空间中,所以他具有三点特征:
1.该静态成员对象是属于类的,而非某个对象的。
2. 该静态成员对象为类的所有对象所共享一份。
3.该静态成员不占用类对象的空间。
下面我们就重点讲一下,类中的静态成员函数。
与静态成员对象类似,静态成员函数也是属于类的,而非某个对象。
但是使用static修饰成员函数与修饰变量不同,他并不是对存储方式的修饰,而是将此成员函数提升为全局函数的特性,由于类的封装性的要求,这个静态成员函数他被隐藏在类域之中。当没有生成对象时,他也是可以调用的,故没有this指针,所以是没有办法调用到类中的成员的。所以类中的静态成员函数是不可访问非静态成员变量或函数的。
但是普通成员对象或函数是可以访问静态成员变量或调用静态函数的
#include <iostream>
using namespace std;
class Stu
{
private:
string name;
int age;
public:
//类中的静态成员属性
static int count;
public:
Stu(string name, int age)
{
this->name = name;
this->age = age;
count++;
}
void showInfo()
{
cout << this->name << "," << this->age << endl;
}
//类中的静态方法
static int get_count()
{
//cout << name << endl;错误,因为静态成员函数没有this指针是无法调用类中非静态的成员属性的。
return count;
}
};
int Stu::count = 0;
int main()
{
Stu zhangsan("zhangsan",18);
cout << zhangsan.get_count() << endl;
cout << Stu::get_count() << endl;
cout << Stu::count << endl;
return 0;
}
静态成员函数特性的总结:
使用static关键字修饰类成员函数时,就是把这个成员函数升级成了全局函数。
只不过是这个全局函数隐藏在了这个类之中而已。
1.静态成员函数是没有this指针的。
2.静态成员函数是不可以调用类中的非静态成员,只能调用或访问类中的静态成员。
3.静态成员函数不依赖于成员对象。可以使用 类名+ 类域访问符的形式直接调用。
所以:静态成员函数与静态成员对象一样,是服务于整个类的,而不依赖于某个对象。
欢迎各位看官指正。