C++学习记录2——类和对象
一、类和对象
1.类和对象概念
万事万物皆为对象,对象上有属性和方法。
具有相同的属性和方法(相同性质)的对象可被抽象称为类。
类中的属性和对象统称为成员
属性也称成员属性,成员变量,也称字段(field);方法也称成员方法,成员函数。
2.类和对象的定义
const double PI = 3.14;
class Circle
{
public: //公共权限(若不写,默认为私有)
int r; //属性:圆的半径(变量)
double calculateS() //方法:求圆的周长(函数)
{
return 2*PI*r;
}
};
int main()
{
//通过圆类,创建具体的圆对象 (实例化)
Circle c1;
//给圆对象的属性进行赋值
c1.r = 10;
cout<<"圆的周长为:"<<c1.calculateS()<<endl;
同一个类中的方法可以直接调用,不同类要用类名.方法
3.struct 和 class 的区别
唯一区别在于默认访问权限不同
- struct默认权限为公有
- class 默认权限为私有
二、封装
1.封装的意义
- 将属性和方法作为一个整体表现,隐藏实现细节,直接调用
class Circle
{
public:
int r; //属性:圆的半径(变量)
double calculateS() //方法:求圆的周长(函数)
{
return 2*PI*r;
}
void getR(int R) //可通过方法为属性赋值
{
r = R;
}
};
- 将属性和行为加以权限控制,保证安全
访问权限:
访问权限有3种:
1.public——公共权限(成员类内类外都可访问)
2.protected——保护权限(成员类内可访问,类外不可访问,子类可访问)
3.private——私有权限(成员类内可访问,类外不可访问,子类不可访问)
class Person
{
public:
string name;
protected:
int age;
private:
int password;
private:
void put() //类内都可访问
{
name = "张三";
age = 20;
password = 123456;
}
};
int main()
{
Person P1;
//P1.age = 10; //类外不可访问
//P1.put();
成员属性的私有化:
优点:
- 将所有成员属性设为私有,可自己控制读写权限
- 对于写权限,可检测数据的有效性
class Person
{
private:
string m_name; //只读
int m_age; //可读可写
int m_password;
public: //通过公共接口操作
string getName() //通过公共函数获取私有属性
{
return m_name;
}
void setAge(int age) //通过公共函数修改私有属性
{
if (age<0) //判断检测数据的有效性
{
cout<<"你个碎皮"<<endl;
return;
}
if (age>150)
{
cout<<"你个老妖精"<<endl;
return;
}
else
{
m_age = age;
}
}
int getAge()
{
return m_age;
}
};
int main()
{
Person P1;
setAge(200);
getAge();
}
作用域运算符:
两个冒号::
将类 Point 创建在头文件里,在源文件中包含,调用Point类中的函数需要用作用域运算符表明作用域。
#include "point.h"
int Point::getX() //表明是Poin作用域下的getX()函数
{
return X;
}
三、对象的初始化和清理
每个对象都有初始设置和对象销毁前的清理数据设置
1.构造函数和析构函数
对象若未初始化,使用后果未知;未及时清理,可能有安全问题
C++中有构造函数和析构函数会被编译器自动调用,完成对象初始化和清理工作。如果我们不提供构造和析构,编译器会提供(强制),但编译器提供的构造和析构是空实现(函数中无代码)。
- 构造函数(也称构造方法或构造器):主要作用在于创建对象时为对象成员属性赋值,自动调用
- 析构函数:对象销毁前系统自动调用,执行一些清理工作
构造函数语法: 类名( ) { }
- 没返回值也不写void
- 函数名称与类名相同
- 构造函数有参数,因此可以发生重载
- 程序调用对象时会自动调用构造,无需手动调用,且只调用1次
析构函数语法: ~类名( ) { }
- 没返回值也不写void
- 函数名称与类名相同,名称前加 ~ 符号
- 析构函数不可以有参数,不可以发生重载
- 程序在对象销毁前会自动调用析构,无需手动调用,且只调用1次
class Person
{
public: //需要公共才可调用
Person()
{
cout<<"构造函数的调用"<<endl;
}
~Person()
{
cout<<"析构函数的调用"<<endl;
}
};
void test1()
{
Person P1; //在栈上的数据,test1执行后会释放对象(销毁)
}
int main()
{
test1(); //创建对象会自动调用Person()
//test1执行后自动调用~Person()
Person P2; //只调用Person()
//main()函数执行完后会调用~Person()
}
2.构造函数的分类及调用
两种分类方式:
- 按参数分为:有参构造和无参构造(默认)
- 按类型分为:普通构造和拷贝构造
三种调用方式:
- 括号法
- 显示法
- 隐式转换法
拷贝构造有参,参数为另一个类,将另一个类的全部属性拷贝到此类上
需要加 const 防止改变原类,需要使用引用传递
class Person
{
int age;
public:
Person(const Person01 &p)
{
cout<<"拷贝构造函数的调用"<<endl;
age = p.age;
}
};
void test1()
{
//1.括号法
Person P1; //调用默认构造函数,不要加(),否则会被认为是函数的声明
Preson P2(10); //调用有参构造函数
Person P3(P2); //调用拷贝构造函数
//2.显示法
Person P1;
Person P2 = Person(10);
Person P3 = Person(P2);
//3.隐式转换法
Person P2 = 10; //相当于Person P2 = Person(10);
Person P3 = P2;
}
int main()
{
test1();
}
PS:
- Person(10) 为匿名对象 Person P2 = Person(10);起名为P2
匿名对象的特点:当前行执行结束后,系统会立即回收匿名对象 - 不要用拷贝构造函数初始化匿名对象
Person(P3)
会被编译器认为是Person P3 //(对象声明)
3.拷贝构造函数的调用时机
通常有三种情况:
- 使用一个已创建好的对象来初始化一个新对象(最常用)
- 值传递的方式给函数参数传值(相当于创建临时拷贝)
class Person
{
int age;
};
void dowork(Person p) //值传递
{
}
int mian()
{
Person P;
dowork(P);
}
- 以值方式返回局部对象
class Person
{
int age;
};
Person dowork1() //局部对象
{
Person P1; //P1执行完后就被释放
return P1; //拷贝一个新对象(P1’)返回给p
}
int mian()
{
Person p = dowork1(); //p不是P1,是P1’
}
4.构造函数的调用规则
默认情况下,C++编译器至少给一个类添加三个函数:
1.默认构造函数 2.默认析构函数 3.默认拷贝构造函数,对属性进行值拷贝
调用规则:
1.如果用户定义有参构造函数,C++不再提供默认无参构造,提供默认拷贝构造
2.如果用户定义拷贝构造函数,C++不再提供其他构造函数
class Person
{
int age;
public:
Person(int age1) //已有有参构造函数
{
cout<<"有参构造函数的调用"<<endl;
age = age1;
}
};
int mian()
{
//Person P; //报错,没有默认构造函数可调用
Person P(10);
}
5.深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
class Person
{
public:
Person()
{
cout << "默认构造函数" << endl;
}
Person(int age,int height)
{
m_age = age;
m_height = new int(height);
cout << "有参构造函数" << endl;
}
~Person()
{
//析构代码,将堆区开辟数据释放
if (m_height != NULL)
{
delete m_height;
m_height = NULL;
}
cout << "默认析构函数" << endl;
}
int m_age;
int* m_height;
};
void test1()
{
Person P1(18,180);
cout << "P1的年龄:" << P1.m_age <<"身高为:"<<P1.m_height<< endl;
Person P2(P1);
cout << "P1的年龄:" << P1.m_age << "身高为:" << P1.m_height << endl;
}
int main()
{
test1();
return 0;
}
以上代码执行会报错,原因在于析构代码2次执行,重复释放空间
解决方法:通过深拷贝,重新开辟空间
Person(const Person &p)
{
m_age =p.m_age;
//m_height = p.m_height; //编译器默认实现
//深拷贝
m_height = new int(*p.m_height);
cout << "拷贝构造函数" << endl;
}
6.初始化列表
用于初始化对象的属性构造函数():属性1(值1),……{}
class Person
{
int age1;
int age2;
};
Person():age1(10),age2(20)
{
}
int main()
{
Person p;
}
或
class Person
{
int age1;
int age2;
};
Person(int a,int b):age1(a),age2(b)
{
}
int main()
{
Person p(20,10);
}
7.类对象作为类成员
类中的成员可以是另一个类的对象,称该成员为对象成员
class A
{
public:
setA()
{
A a;
}
};
class B
{
int n;
A a; //对象成员
};
当其他类的对象作为本类成员时,构造时先构造类对象,再构造自身,析构时先析构自身,再析构类对象
8.静态成员
成员变量、成员函数前加static
静态成员变量:
所有对象共享同一份数据,在编译阶段分配内存,类内声明,类外初始化
静态成员函数:
所有对象共享同一个函数,静态成员函数只能访问静态成员变量
静态成员函数也有访问权限
class Person
{
public:
static void fun()
{
a=10;
//b=10; //错误
}
static int a;
int b; //属于特定对象,静态函数调用无法区分是哪个对象的b
};
int a = 0;
int main()
{
Person p; //通过对象访问
p.fun(); //类外初始化
Person::fun(); //通过类名访问,不创建对象也可访问
}
四、C++对象模型与this指针
1.成员变量和成员函数分开存储
类内成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上
空类空对象占用内存空间为:1字节
编译器会给每个空对象分配1字节空间,为了区分空对象占内存的位置。每一个空对象也有独一无二的内存地址。
class Person
{
public:
void fun() //(静态/非静态)成员函数,不属于类的对象上
{}
static int a; //静态成员变量,不属于类的对象上
int b; //只有非静态成员变量,属于类的对象上
};
int Person::b=0; //类外初始化
int main()
{
Person p;
}
上面的Person p占用空间为4字节(只有int b)
2.this指针
每个非静态成员函数只会诞生一份函数实例,多个同类型的对象会公用一块代码。
C++提供特殊的对象指针:this指针,指向被调用的成员函数所属的对象
this指针隐含在每一个非静态成员函数内,不需定义,可直接使用
this指针的本质是指针常量,指向不可修改
用途:
- 当形参和成员变量同名时,可用this指针区分
class Person
{
public:
Person(int age) //这前三个age会被认为是同一个量
{
age = age; //输出乱码
this->age = age; //正确,this age 与下面 int age 相同
}
int age;
int m_age; //区分变量名,member age
};
int main()
{
Person p1(18);
cout << "p1的年龄:" << p1.age << endl;
}
- 在类的非静态成员函数中返回对象本身,可使用return * this
class Person
{
public:
Person(int age)
{
this->age = age;
}
Person& PersonAge(Person& p) //用引用方式返回,会一直返回p2,值方式会创建新的临时对象
{
this->age += p.age;
return *this;
}
int age;
};
void test1()
{
Person p1(10);
Person p2(20);
//链式编程思想,每次都返回p2可实现
p2.PersonAge(p1).PersonAge(p1).PersonAge(p1); //值传递无法实现
}
int main()
{
test1();
return 0;
}
PS:
p2.PersonAge(p1).PersonAge(p1).PersonAge(p1);
这样的连续追加的链式编程思想与cout << "a=" << a << endl;
的本质相同
实现需要每次返回值都为本身(p2/cout)
3.空指针访问成员函数
空指针也可调用成员函数,但要注意有没有用到this指针,如果用到this指针,通常要加以判断保证代码健壮性。
class Person
{
public:
void getage()
{
if (this ==NULL) //防止指针为空
return;
//m_age 即 this->m_age 传入指针不能为空
cout << "age:" << m_age << endl;
}
int m_age;
};
void test2()
{
Person* p = NULL;
p->getage();//报错,因为传入空指针,无法访问属性m_age()
}
int main()
{
//test1();
return 0;
}
4.const修饰成员函数
成员函数后加const修饰后称常函数,常函数内不可修改成员属性,成员属性声明时加关键字mutable后,在常函数中依然可以修改,在常对象下也可以修改
声明对象前加const称为常对象,常对象只能调用常函数(普通成员函数可能修改属性)
class Person
{
public:
void getage() const
{
this->m_age=10; //加const不可修改
cout << "age:" << m_age << endl;
}
int m_age;
//mutable int m_age; //加mutable修饰则可修改
};