类和对象
C++面向对象的三大特性:封装、继承和多态。
数据成员的访问
class Point{
double x,y;
public:
int a,b;
protected:
int c,d;
};
使用class关键字与struct不同,成员在默认情况下是私有的。
- public:公有,可以由类的用户访问,
- private:私有,除了对自身的成员函数外都不可见。
- protected:保护,可以在它的类的子类中被访问。
定义成员函数
如果不想要用户直接访问数据成员,但又可以控制设置它的值,解决方案是使数据成员私有,并且依靠成员函数来提供间接访问。
class Point{
public:
//内联定义函数
void set_x(doublex newx){x = newx;}
void set_y(double newy){y = newy;}
double get_x(){return x;}
double get_y(){return y;}
private:
double x, y;
};
//在类声明之外定义
void Point::set_x(doublex newx){x = newx;}
void Point::set_y(double newy){y = newy;}
double Point::get_x(){return x;}
double Point::get_y(){return y;}
函数是公开的,可以被用户访问。数据成员是私有的,不能访问。
C++提供两种方式定义成员函数:
- 内联,一个成员函数可以通过类本身中提供定义,被内联定义。函数原型紧跟要执行的语句的大括号{},与用inline关键字修饰的全局函数一样,内联函数的函数体被直接扩展到调用函数的主体中。(优化运行性能,但是占用更多内存,适用于简短函数)
- 一个成员函数可以在类声明之外定义。在这种情况下,函数定义要求用作用域符(::)来说明函数的作用域。
格式:
返回类型 类名::函数名(参数){ 语句组 }
在类声明之外定义一个成员函数能使你编写任意长的函数,而无需让类声明本身来负担。
调用成员函数
两种方式:
- 对象.函数(参数)
- 通过指向对象的指针:指针->函数(参数)
私有成员函数
私有成员函数和私有数据成员一样,对外隐藏,只能被类中其他函数调用。
函数成员还可以用protected被做成受保护的,此时类的子类可以参考甚至修改(重载)此函数。private和protected的区别只与涉及子类的地方有关
构造函数和析构函数
构造函数是一个初始化函数,当一个对象在内存中被分配后,它会自动被调用。它与类的名称相同,且无返回值。与其他函数一样,可以被内联,也可以在类外定义。
在其他成员函数中可以调用构造函数,但是只是产生一个匿名对象,对本对象并无影响
析构函数是清理函数。作用在对象销毁前系统自动调用,执行清理操作。
都只会被调用一次。
构造函数成员初始化列表
如果Classy是一个类,而mem1、mem2和mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员:
Classy::Classy(int n, int m) :mem1(n),mem2(0),mem3(n*m+2){
...
}
上述代码将mem1初始化为n,mem2初始化为0,将mem3初始化为n*m+2,从概念上说,初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码。注意事项:
- 这种格式只能用于构造函数
- 必须用这种格式初始化非静态const数据成员(至少在C++11之前)
- 必须用这种格式初始化 引用& 数据成员。
- 数据成员初始化的顺序与它们出现在类中的声明顺序相同,与排列顺序中的顺序无关。
必须用带有初始化列表的构造函数:
1.成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
2.const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。
#include <iostream>
using namespace std;
//A 没有默认构造函数(无参构造函数)
class A
{
public:
A(int k);
static int e;
private:
const int a=1;
const int b;
int & c;
};
int d = 5;
A::A(int k):b(2),c(d)
{
//a = 1;
cout << "a = "<<a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << "k = " << k << endl;
}
//静态成员变量的初始化: 类型+ 使用类名+作用域限定符
int A::e = 6;
class B
{
public:
B();
B(int k);
private:
A a;
int f;
};
//因为a无默认构造函数,所以必须在初始化列表中初始化,给a传入参数
B::B(int k):a(k)
{
}
B::B() : a(6)
{
}
int main(void) {
A a(7);
cout << "A::e = "<<A::e << endl;
cout << "a.e = " << a.e << endl;
cout << "---------------------" << endl;
B b(9);
return 0;
}
/*
*结果:
a = 1
b = 2
c = 5
k = 7
A::e = 6
a.e = 6
---------------------
a = 1
b = 2
c = 5
k = 9
*/
在构造函数中使用new的注意事项
- 如果构造函数中使用的new动态分配内存,则必须提供使用delete的析构函数。
- new和delet必须相互兼容。new对应delete,new[ ] 对应于delete[]。
- 如果有多个构造函数,则必须以相同的方式使用new和delete,因为只有一个析构函数,所有构造函数必须跟它兼容。
this指针
this指针指向被调用对象本身。每个非静态成员函数(包括构造函数和析构函数)都有一个this指针。this是调用对象的地址。*this才是被调用对象(解引用)。所以可以使用 this->a,或者(*this).a来调用被调用对象的数据和函数。
const成员函数
保证函数不会修改调用对象。
cosnt Stock land = Stock("Test");
land.show();
对于当前的编译器来说,会对第二行报错,因为show的代码无法保证调用对象不被修改。我们以前通过将函数参数声明为const引用,或者指向const的指针来解决这种问题。但是这里show()方法没有任何参数,它所使用的对象由方法调用隐式地提供。C++的解决方法是将const关键字放在函数括号的后面:
//函数声明
void show() cosnt;
//函数定义
void Stock::show() const
以这种方法声明和定义的函数被称为const成员函数,只要类方法不修改调用对象,就应该将其声明为const。
注意:
- 常成员函数不能调用其他非常成员函数(静态函数除外)
- 常成员函数可以修改静态成员变量
- const对象只能调用常成员函数
- 两个成员函数的名字和参数表相同,但一个是 const 的,一个不是,则它们算重载
#include <iostream>
#include <vector>
using namespace std;
class A
{
public:
A(int aa = 0):a(aa) {
}
static int b;
void show()const {
cout << ++b << endl;
setA1();
cout << "show const" << endl;
//setA();错误
}
void show() {
cout << ++b << endl;
setA1();
cout << "show" << endl;
//setA();错误
}
void setA() {
cout << ++a << endl;
}
static void setA1() {
cout << ++b << endl;
}
private:
int a;
};
int A::b = 1;
int main(void) {
A a;
const A ca;
ca.show();
a.show();
cout << a.b << endl;
cout << ca.b << endl;
return 0;
}
/*
2
3
show const
4
5
show
5
5
*/
运算符重载
友元函数
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
创建友元函数:
第一步是将其原型放在类声明中,并在类声明前加上关键字 friend:
friend Time test(double n,Time &t);
- 虽然该函数是在类中声明的,但它不是成员函数,因此不能用成员运算符调用。
- 虽然友元函数不是成员函数,但是它与成员函数的访问权限相同。
第二步是在类外编写函数的定义,不需要在定义中使用关键字friend,也不需要使用::限定符。
拷贝构造函数
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
- 用同一类的一个已知的对象去新建并初始化该类的另一个对象。
- 复制对象把它作为参数传递给函数,值传递—参数为对象。
- 复制对象,并从函数返回这个对象—返回值为对象。
- 编译器生成临时对象
如果在类中没有定义拷贝构造函数,编译器会自行定义一个(浅拷贝)。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数(深拷贝)。拷贝构造函数的最常见形式如下:
//const关键字是为了防止被拷贝的对象被修改
//使用引用的方式传递参数
classname (const classname &obj) {
// 构造函数的主体
}
#include <iostream>
using namespace std;
class Line
{
public:
int getLength(void);
Line();
Line(int len); // 简单的构造函数
Line(const Line &obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int *ptr;
};
// 成员函数定义,包括构造函数
Line::Line() {
cout << "调用无参构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = 0;
}
Line::Line(int len)
{
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line &obj)
{
cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void)
{
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength(void)
{
return *ptr;
}
void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() << endl;
}
Line g() {
Line line3(18);
return line3;
}
// 程序的主函数
int main()
{
Line line1(10);//调用普通构造函数
Line line2 = line1;//情况1,同Line line2 = Line(line1);用类的一个已知的对象去初始化该类的另一个对象时,需要调用拷贝构造函数
display(line1);//情况2,对象作为参数传递给函数,需要调用拷贝构造函数
display(line2);//情况2
Line line3 = g();//情况3,从函数返回对象
display(line3);//情况2
return 0;
}
/*
结果:
调用构造函数 //Line line1(10);
调用拷贝构造函数并为指针 ptr 分配内存 //Line line2 = line1;/
调用拷贝构造函数并为指针 ptr 分配内存 //display(line1);
line 大小 : 10 //display(line1);
释放内存 //display(line1);
调用拷贝构造函数并为指针 ptr 分配内存 //display(line2);
line 大小 : 10 //display(line2);
释放内存 //display(line2);
调用构造函数 //Line line3 = g();
调用拷贝构造函数并为指针 ptr 分配内存 //Line line3 = g();
释放内存 //Line line3 = g();
调用拷贝构造函数并为指针 ptr 分配内存 //display(line3);
line 大小 : 18 display(line3);
释放内存 display(line3);
释放内存 //line3
释放内存 //line2
释放内存 //line1
*/
不要用拷贝构造函数初始化匿名对象:
Line line1(10);
//错误,“Line line1”: 重定义
//编译器会认为 Line (line1) == Line line1
Line(line1);
隐式转换法
Line line1 = 10;//等于Line line1 = Line(10);
默认情况下,C++编译器会给一个类添加三个函数:
- 默认构造
- 默认拷贝构造
- 默认析构
如果我们写了有参构造函数,编译器就不再提供默认构造;但会提供默认拷贝函数。
如果我们写了拷贝构造函数,编译器就不再提供其他构造函数;
拷贝构造 > 有参构造 > 默认构造
深拷贝与浅拷贝
默认的拷贝构造函数—浅拷贝。复制的是成员的值。按字节拷贝,当对象的数据资源是由指针指向的堆时,默认的拷贝构造函数只是将指针复制。 ,多个对象共用同一块资源,同一块资源释放多次,崩溃或者内存泄漏
深拷贝:深拷贝会重新开辟内存空间。,每个对象拥有自己的资源,必须显式提供拷贝构造函数和赋值运算符。
静态成员变量
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
class Point
{
public:
static int npoints;
//特殊情况,用const和static一起修饰,只可以在类中声明一次
const static int a = 0;
};
//static变量必须在类外定义一次,并只定义一次
int Point::npoints = 0;
int main(void) {
Point p1, p2, p3;
Point::npoints++;
p1.npoints++;
p2.npoints += 5;
cout << p3.npoints << endl;//p3.npoints == 7
return 0;
}
注意:不能在类声明中初始化静态成员变量,这是因为声明只描述了如何分配内存,但并不分配内存。例外情况:静态数据成员为const整数类型或枚举型。
数据通常是由单个对象所拥有的,而不是类拥有。然而它可以声明由同一个类的所有对象共享的一个或多个数据成员,无论创建了多少对象,程序都只创建一个静态类变量副本,这样的数据成员称为静态的。
成员函数也可以是静态的,静态成员函数的定义和声明和其他函数一样,但是用途受限,不能引用this指针,不能引用非静态成员函数。
静态成员可以是公有私有和受保护的。
class Student{
public:
Student(char *name, int age, float score);
void show();
public:
static int m_total; //静态成员变量
private:
char *m_name;
int m_age;
float m_score;
};
static 成员变量属于类,不属于某个具体的对象,即使创建多个对象,也只为 m_total 分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了 m_total,也会影响到其他对象。
static 成员变量必须在类声明的外部初始化,具体形式为:
type class::name = value;
type 是变量的类型,class 是类名,name 是变量名,value 是初始值。将上面的 m_total 初始化:
int Student::m_total = 0;
静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。
注意:static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
static 成员变量既可以通过对象来访问,也可以通过类来访问。
static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。
总结:
-
一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
-
static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
-
静态成员变量必须初始化,而且只能在类体外进行。例如:
int Student::m_total = 10;
初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值 0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。 -
静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。
静态成员函数
静态成员函数(static)不能通过对象来调用,也不能使用this指针。函数声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符::来调用它。
//在Test类中声明和定义
static int howmany(){ return num_string;}
//调用:
int count = Test::howmany;
由于静态成员函数不与特定的对象相关联。因此只能使用静态数据成员。