C++ 类与对象
目录
- 1 类的定义
- 2 访问限定符
- 3 类域
- 4 对象
- 4.1 实例化对象
- 4.2 对象的大小
- 4.3 this 指针
- 5 运算符重载
- 6 const 成员函数
- 7 默认成员函数
- 7.1 构造函数
- 7.2 析构函数
- 7.3 拷贝构造函数
- 7.4 赋值运算符重载
- 7.5 取地址运算符重载
- 8 初始化列表
- 9 隐式类型转换
- 10 static 关键字
- 11 友元声明
- 12 内部类
1 类的定义
C++中,使用 class 关键字或 struct 关键字可以定义一个类,在类中,可以像命名空间一样写上变量,函数等,类中的变量被称为成员变量或属性,类中的函数被称为成员函数或方法,类中的函数默认为内联函数
使用 class 定义类
//Date类
class Date
{
public:
//成员函数void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:
//成员变量int _year;int _month;int _day;
};
使用 struct 定义类
struct Person
{
public:void Print(){cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;}
private:double _height;int _weight;
};
在成员变量的名称前面加上 _ 是为了与外界传入的参数作区分
2 访问限定符
C++在定义类时,可以使用相应的访问限定符来限制成员变量和成员函数的访问权限,它们分别是:
private:限制只能在类内部访问
protected:限制只能在类内部访问
public:可在类的内部和类的外部访问
在 class 定义的类中,如果没写访问限定符,成员默认为 private
在 struct 定义的类中,如果没写访问限定符,成员默认为 public
访问限定符的有效范围是从上一个访问限定符开始,到下一个访问限定符为止
通过合理地使用访问限定符,可以提高代码的安全性,一般会将成员变量设定为 private ,需要对外开放的成员函数则为 public
3 类域
类的大括号内部的区域,被称为类域,类中的成员都从属于这个区域
在类域内定义成员:
直接写在类的内部即可
成员函数定义在类内部时,默认是内联函数(inline)
成员变量需要先声明,再在构造函数中进行初始化(定义)
在类域外定义成员:
需要用域作用限定符指定域名
成员函数需要在类中先声明,再在类外定义,定义在类外部时,不是内联函数(inline)
成员变量需要先声明并使用 static 修饰,再在类外定义
class Person
{
public:void Print();
private:double _height; //成员变量的声明而非定义int _weight;int _age;
};void Person::Print()
{cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;
}int main()
{return 0;
}
类域影响的是编译时的查找方式,上面的代码中,如果 Print() 没有指明 Person 类,那么编译时就会去全局域查找变量 _height 和 _weight ,如果指明了 Person 类,那么编译时就会去 Person 类域中查找
4 对象
4.1 实例化对象
使用类名来定义一个变量的操作,就被称为实例化对象
一个类可以用来实例化多个对象
class Person
{
public:void Print(){cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;}
private:double _height;int _weight;int _age;
};int main()
{//实例化对象Person P1;Person P2;Person P3;return 0;
}
对象中有类中声明的成员变量和定义的成员函数,可以通过 . 操作符来访问,但是会受到访问限定符的限制
int main()
{Person P1;P1.Print(); //访问成员函数P1._height; //_height为private修饰的成员变量,无法在类外访问return 0;
}
4.2 对象的大小
对象大小的计算与 C 语言结构体类型变量的计算相同,在计算时,不需要计入成员函数,因为每个对象所使用的成员函数都是一样的
计算规则为:
- 第一个成员变量从存放位置偏移量为 0 的位置开始存放
- 剩下的成员变量,要从偏移量 = 对齐数的整数倍处开始存放,对齐数为编译器默认对齐数和变量大小的小值
- 对象的总大小为所有成员变量中,最大对齐数的整数倍
- 如果有内部类,内部类不计入大小
例:计算下面对象 a 的大小
class A
{
public:void Print(){cout << _ch << endl;}
private:char _ch;int _i;
};int main()
{A a;return 0;
}
成员函数不计入大小
成员变量中:
_ch 是第一个成员变量,所以从偏移量为 0 的位置开始存放
_i 为 4B,默认对齐数是 8,取小值 4,所以需要从偏移量 = 4 的整数倍的位置开始存放
因此,a 的存储形式是这样的:
整体为 8 B
需要注意的是,如果对象没有成员变量,那么对象默认是 1 B
比如下面的这个对象:
class B
{
public:void Print(){//...}
};
4.3 this 指针
类中的成员函数在定义时,参数列表中隐含了 this 指针,它用来指向当前的对象
this 指针的类型为 类名* const ,也就意味着它是不能更改的
比如下面的 P1 对象在调用 Print() 时,Print() 的 this 指针类型是 Person* const,指向了 P1
class Person
{
public://隐含了指针 this --> Person* const thisvoid Print(){cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;}
private:double _height;int _weight;int _age;
};int main()
{Person P1;P1.Print();return 0;
}
在类的成员函数内部,可以显示使用 this 指针来使用成员变量
class Person
{
public:void Print(){cout << "height: " << this->_height << endl; //显示使用 this 指针cout << "weight: " << this->_weight << endl;}
private:double _height;int _weight;int _age;
};
在成员函数的形参中,不能显示给出 this 指针,并且在实参中,不能显示传入当前对象的地址
class Person
{
public:void Print(Person* const this) //形参不能显示给出 this{cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;}
private:double _height;int _weight;int _age;
};
int main()
{Person P1;P1.Print(&P1); //实参不能显示传入当前对象地址return 0;
}
5 运算符重载
C++中,由于对象的成员较多,为了适应对象的复杂运算,支持在类的内部对运算符进行重载,相当于赋予其某些运算规则,在使用对象进行运算时,就会自动调用相应的运算符重载
运算符重载的特点主要有:
- 运算符重载是一种函数,名称固定为 operator 要重载的运算符,它具有返回值,参数列表,函数体
- 如果重载的运算符是单目运算符,则形参只需要一个,重载的运算符是双目运算符,则形参需要两个
- 运算符重载时,至少要有一个类类型的参数
- 如果运算符重载是成员函数,则参数会减少一个,因为成员函数会隐含 this 指针
- 运算符重载不改变原运算符的优先级和结合性
- ?: :: .* . sizeof 是不能被重载的运算符
- 不能重载不存在的运算符,比方说 operator$
- 在重载 前置++ 和 后置++ 时,为了进行区分,C++规定前置++的重载写为 operator++(),而 后置++的重载写为 operator++(int)
- 在重载 流提取运算符 << 和 流插入运算符 >> 时,最好写为全局函数而非成员函数,这样就不会导致第一个参数被默认的 this 指针所占据,可以写 ostream或istream 的对象,更加符合使用习惯
class Person
{
//友元声明friend ostream& operator<<(ostream& out, Person& person);friend istream& operator>>(istream& in, Person& person);
public:void Print(){cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;cout << "age: " << _age << endl;}//重载单目运算符//前置++Person& operator++(){_age++;return *this;}//后置++Person& operator++(int){Person tmp = *this;_age++;return tmp;}//重载双目运算符Person& operator+=(double height){_height += height;return *this;}private:double _height;int _weight;int _age;
};//重载<<
ostream& operator<<(ostream& out, Person& person)
{out << person._age << "|" << person._height << "|" << person._weight << endl;return out;
}//重载>>
istream& operator>>(istream& in, Person& person)
{in >> person._age >> person._height >> person._weight;return in;
}int main()
{Person person;cin >> person;cout << person;return 0;
}
6 const 成员函数
被 const 修饰的成员函数是 const 成员函数,const 要写到成员函数参数列表的后方
此时,const 本质是在修饰 this 指针,this 指针的类型变成了 const 类名* const,也就是说,this 本身的指向无法更改,this 指向的内容也是不能更改的
因为普通成员函数的 this 指针指向的内容是可以修改的,但是 const 对象的内容是不能更改的,如果 const 对象调用了普通的成员函数,就发生了权限放大的情况,所以 const 对象只能调用 const 成员函数,但是普通对象都可以调用,调用普通成员函数时,相当于权限平移,调用 const 成员函数时,相当于权限缩小
class Person
{
public:
//const 成员函数,this-->const Person* const thisvoid Print() const{cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;cout << "age: " << _age << endl;}
private:double _height;int _weight;int _age;
};
int main()
{Person person; //普通对象person.Print();const Person person2; //const对象person2.Print();return 0;
}
7 默认成员函数
在类的内部有几种函数,如果用户不显示给出,编译器会默认生成,它们被称为默认成员函数
默认成员函数主要包括构造函数,析构函数,拷贝构造函数,赋值重载函数,普通对象取地址重载,const 对象取地址重载
7.1 构造函数
构造函数是在定义对象时会调用的函数,它用于对象的初始化
构造函数的特点主要有:
- 构造函数没有返回值
- 构造函数的名称和类名相同
- 构造函数可以进行重载
- 当用户没有显示实现构造函数时,编译器会自动生成一个构造函数
- 全缺省,无参,编译器自动生成的构造函数都被称为默认构造函数,它们都可以不用传入实参,没有传入实参时,全缺省的构造函数使用缺省值,传入实参时,全缺省的构造函数使用传入值
- 对于内置类型的成员变量,编译器自动生成的构造函数是否进行初始化是不确定的
- 对于自定义类型的成员变量,如果它有自己的默认构造函数,编译器自动生成的构造函数会去调用该成员变量的默认构造函数,如果没有则会报错
一般来说,构造函数无论在什么情况下都需要写,保证成员变量的初始化
在实例化对象时,如果不需要传实参,就不需要在对象名称后加括号,默认构造函数存在,这样调用的就是默认构造函数,默认构造函数不存在,则会报错。如果需要传实参,就需要在对象名称后加上括号并写参数,这样就会自动匹配需要的构造函数
class Person
{
public://默认构造1 -- 无参//Person()//{////}//默认构造2 -- 全缺省Person(double height = 1.80, int weight = 90){_height = height;_weight = weight;}//非默认构造//Person(double height, int weight)//{// _height = height;// _weight = weight;//}void Print(){cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;}
private:double _height;int _weight;int _age;
};int main()
{Person P1; //不传实参,默认构造函数存在Person P2(1.60, 95); //传入实参,自动匹配P1.Print();P2.Print();return 0;
}
此处 Person 没有显示给出构造函数,对于内置类型的成员变量,编译器可能进行初始化,也可能不进行初始化,对于自定义类型的成员变量 d ,由于它有自己的默认构造函数,所以编译器自动生成的构造函数会去调用它的默认构造函数 Date()
class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};class Person
{
public:void Print(){cout << "height: " << _height << endl;cout << "weight: " << _weight << endl;}
private:double _height;int _weight;int _age;Date d; //自定义类型成员变量,调用它的默认构造
};int main()
{Person P1;P1.Print();return 0;
}
结果:
7.2 析构函数
析构函数是在对象生命周期结束时进行调用的函数,它主要用于对象的销毁
析构函数的特点主要有:
- 析构函数没有返回值
- 析构函数的名称为 ~类名
- 析构函数不可以重载
- 当用户没有显示实现析构函数时,编译器会自动生成一个析构函数
- 对于内置类型的成员变量,编译器自动生成的析构函数并不会进行处理
- 对于自定义类型的成员变量,编译器自动生成的析构函数会调用它的析构函数
- 调用析构函数时,是根据对象的定义顺序反向调用的,最晚定义的对象,它的析构函数是第一个调用的,而最早定义的对象,它的析构函数是最后一个调用的
对于析构函数,如果在成员变量中,有指向型资源,比如动态开辟的数组,那么就需要用户自己给定析构函数来对其进行释放
class Stack
{
public:Stack(){int _capacity = 4;_a = (int*)malloc(sizeof(int) * _capacity);if (_a == NULL){perror("malloc fail");return;}_top = 0;}//自己给定析构对资源进行释放~Stack(){free(_a);_a = NULL;_capacity = 0;_top = 0;}
private:int* _a; //资源int _top;int _capacity;
};int main()
{Stack s;return 0;
}
7.3 拷贝构造函数
拷贝构造函数主要用于使用另一个已经存在的对象来对还未初始化的对象进行初始化
拷贝构造函数的特点主要有:
- 拷贝构造函数是一种构造函数
- 拷贝构造函数没有返回值
- 拷贝构造函数的名称和类名相同
- 拷贝构造函数的第一个参数一定是类类型对象的引用
- 用户没有显示实现拷贝构造函数时,编译器会自动生成一个拷贝构造函数
- 对于内置类型的成员变量,编译器自动生成的拷贝构造函数只会进行值拷贝/浅拷贝
- 对于自定义类型的成员变量,编译器自动生成的拷贝构造函数会调用它的拷贝构造函数
如果成员变量有指向型的资源,比如动态开辟的数组,此时用户需要自己来实现拷贝构造进行深拷贝(开辟新空间,将值进行拷贝),否则默认的拷贝构造只会进行浅拷贝(复制地址)
浅拷贝
class Stack
{
public:Stack(){int _capacity = 4;_a = (int*)malloc(sizeof(int) * _capacity);if (_a == NULL){perror("malloc fail");return;}_top = 0;}~Stack(){free(_a);_a = NULL;_capacity = 0;_top = 0;}//未显示给出时,使用的拷贝构造函数的形式,实际上并不会写出来Stack(Stack& rs){_a = rs._a;_top = rs._top;_capacity = rs._capacity;}private:int* _a;int _top;int _capacity;
};int main()
{Stack s;Stack s2(s); //等价于Stack s2 = sreturn 0;
}
此时,s 和 s2 中的 _a 所使用的空间相同,当进行析构时,指向的空间会被析构两次,就会出错
深拷贝
class Stack
{
public:Stack(){int _capacity = 4;_a = (int*)malloc(sizeof(int) * _capacity);if (_a == NULL){perror("malloc fail");return;}_top = 0;}~Stack(){free(_a);_a = NULL;_capacity = 0;_top = 0;}Stack(Stack& rs){_a = (int*)malloc(sizeof(int) * _capacity);if (_a == NULL){perror("malloc fail");return;}memcpy(_a, rs._a, sizeof(int) * _capacity);_top = rs._top;_capacity = rs._capacity;}private:int* _a;int _top;int _capacity;
};int main()
{Stack s;Stack s2(s); //等价于Stack s2 = sreturn 0;
}
进行深拷贝后,s 和 s2 中的 _a 所使用的空间不相同,所以析构时不会出错
7.4 赋值运算符重载
赋值运算符重载主要用于两个已存在对象间的赋值操作
赋值运算符重载的特点主要有:
- 赋值运算符重载一定要重载为成员函数
- 赋值运算符重载有返回值,返回值建议写成当前类类型的引用,用来连续赋值
- 赋值运算符重载的两个参数都是类类型,建议写成 const 引用来减少拷贝
- 赋值运算符重载若没有显示给出,那么会调用编译器自动生成的赋值运算符重载
- 对于内置类型的成员变量,编译器自动生成的赋值运算符重载只会进行值拷贝/浅拷贝
- 对于自定义类型的成员变量,编译器自动生成的赋值运算符重载会调用它的赋值运算符重载
与拷贝构造函数类似,如果成员变量有指向型的资源,比如动态开辟的数组,此时用户需要自己来实现赋值运算符重载进行深拷贝(开辟新空间,将值进行拷贝),否则默认的赋值运算符重载只会进行浅拷贝(复制地址),容易发生对同一个空间析构两次的问题
class Stack
{
public:Stack(){int _capacity = 4;_a = (int*)malloc(sizeof(int) * _capacity);if (_a == NULL){perror("malloc fail");return;}_top = 0;}~Stack(){free(_a);_a = NULL;_capacity = 0;_top = 0;}//赋值运算符重载Stack& operator=(Stack& rs){//深拷贝_a = (int*)malloc(sizeof(int) * _capacity);if (_a == NULL){perror("malloc fail");return;}memcpy(_a, rs._a, sizeof(int) * _capacity);_top = rs._top;_capacity = rs._capacity;return *this;}private:int* _a;int _top;int _capacity;
};int main()
{Stack s;Stack s2;s2 = s; //等价于s2.operator=(s)return 0;
}
7.5 取地址运算符重载
取地址运算符重载主要有 普通取地址运算符重载和 const 取地址运算符重载,它们都不需要我们自己来实现,因为编译器自动生成的已经足够日常使用
大致形式为:
class Person
{
public:Person():_height(1.60),_weight(45),_age(18){}//普通取地址运算符重载Person* operator&(){return this;}//const 取地址运算符重载const Person* operator&() const{return this;}private:double _height;int _weight;int _age;
};int main()
{Person person;const Person person2;cout << &person << endl;cout << &person2 << endl;return 0;
}
8 初始化列表
初始化列表是位于构造函数参数列表之下,大括号之前,用来对成员变量进行初始化的一种方式
初始化列表以冒号开始,以逗号将成员变量进行分隔,在成员变量名称后加上括号,括号内是初始化的值
class Person
{
public:
//初始化列表Person(int height = 1.60, int weight = 45, int age = 18):_height(height),_weight(weight),_age(age){}
private:double _height;int _weight;int _age;//char _name[20] = "zhangsan"; //声明时给缺省值
};
初始化列表的特性主要有:
- 在初始化列表中,每个成员变量只能出现一次,因为在初始化时,相当于是定义了一个成员变量,一个变量不能定义两次
- 引用成员变量,const修饰的成员变量,没有默认构造函数的自定义类型成员变量,一定要在初始化列表进行初始化
- 初始化列表中成员变量的初始化顺序与它们的声明顺序一致
- 在C++11中,可以在成员声明的位置给缺省值,这是提供给初始化列表使用的功能
- 对于内置类型,初始化列表中没有显示将它初始化,有缺省值,会使用缺省值初始化,如果没有缺省值则取决于编译器
- 对于自定义类型,初始化列表中没有显示将它初始化,会使用默认构造函数来进行初始化,如果没有默认构造函数就会报错
9 隐式类型转换
C++ 可以将 内置类型 或 类类型 的对象 隐式类型转换为 其它类类型 的对象,但是需要对应的构造函数
如果不想要进行隐式类型转换,则可以在构造函数之前添加 explicit 关键字
class A
{
public:A(int a = 1):_a(a){}A(int a, int aa):_a(a),_aa(aa){}//关闭隐式类型转换//explicit A(int a, int aa)// :_a(a)// ,_aa(aa)//{}int getValue() const{return _a;}
private:int _a;int _aa;
};class B
{
public:B(int b = 1):_b(b){}B(const A& a):_b(a.getValue()){}
private:int _b;
};int main()
{//隐式类型转换A a = 1;B b = a;A a2 = { 1, 1 }; //多参数写法return 0;
}
上面代码中,用 1 来初始化 a 对象时,两边类型不同,发生了隐式类型转换,此时会用 1 先产生一个临时对象,再用临时对象拷贝构造初始化 a 对象,用 a 对象来初始化 b 对象时是同理的
10 static 关键字
在类中,可以用 static 关键字对 成员变量 和 成员函数 进行修饰
被 static 修饰的成员变量:
- 被叫做 静态成员变量
- 只能在类外进行初始化
- 所有当前类的对象共用一个 static 成员变量,该成员变量位于静态区内
- 受到访问限定符的限制
- 不能在声明时给缺省值,因为缺省值是给初始化列表用的,初始化列表是初始化单个对象用的,静态成员变量不属于任何一个对象
- 静态成员变量可以使用 类名:: 的方式访问,也可以用 对象. 的方式访问
被 static 修饰的成员函数:
- 被叫做 静态成员函数
- 由于不属于任何一个对象,所以没有隐含 this 指针
- 因为没有 this 指针,所以只能访问静态成员,不能访问当前对象的其它成员
- 可以使用 类名:: 的方式访问,也可以用 对象. 的方式访问
- 非静态成员函数既可以访问静态成员又可以访问非静态成员
class Person
{
public:Person(int height = 1.60, int weight = 45, int age = 18):_height(height), _weight(weight), _age(age){}//非静态成员函数既可以访问静态成员又可以访问非静态成员void Print(){cout << "height:" << _height << endl;cout << "weight:" << _weight << endl;cout << "age:" << _age << endl;cout << "name:" << name << endl;}//静态成员函数只能访问静态成员static char* getName(){return name;}private:double _height;int _weight;int _age;static char name[20];
};//静态成员变量只能在类外进行初始化
char Person::name[20] = "zhangsan";int main()
{Person person;//对象. 类名:: 访问静态成员cout << person.getName() << endl; cout << Person::getName() << endl;return 0;
}
11 友元声明
友元声明可以绕过访问限定符去访问类内部的成员变量和成员函数,要进行友元声明,需要在函数或类的前面加上 friend 关键字,将该声明放入另一个类中即可
class Person
{//友元函数声明friend void Print(Person person);//友元类声明friend class Date;
public:Person(int height = 1.60, int weight = 45, int age = 18):_height(height), _weight(weight), _age(age){}private:double _height;int _weight;int _age;
};class Date
{
public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(Person person){cout << _year << "/" << _month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};void Print(Person person)
{cout << "height:" << person._height << endl;cout << "weight:" << person._weight << endl;cout << "age:" << person._age << endl;
}
友元声明的特性主要有:
- 友元函数可以访问类的私有和保护成员 (private, protected)
- 友元函数的声明可以写在类内部的任意位置
- 一个函数可以是多个类的友元函数
- A 如果是 B 的友元类,那么 A 中的成员函数是 B 的友元函数
- 友元没有传递性,A 是 B 的友元,B 是 C 的友元,但 A 不是 C 的友元
- 友元没有交换性,A 是 B 的友元,但 B 不是 A 的友元
使用友元会使函数与类,类与类的关系(或称耦合度)加强,不适合多次使用
12 内部类
在类 A 的内部定义另一个类 B ,类 B 就是 类 A 的内部类
class Person
{
public:Person(int height = 1.60, int weight = 45, int age = 18):_height(height), _weight(weight), _age(age){}//Date是Person的内部类class Date{public:Date(int year = 2000, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};private:double _height;int _weight;int _age;
};
内部类的特点主要有:
- 定义外部类的对象时,不会包含内部类,所以在计算外部类大小时,不计入内部类
- 内部类是一个独立的类,它只受到外部类类域和访问限定符的限制
- 内部类 是 外部类 的友元类
- 一般来说,如果类 B 的功能设计出来就是给类 A 使用的,那么就会让类 B 成为 类 A 的内部类,如果有必要,可以使用 private 或 protected 修饰类 B,使其只属于外部类 A