本文全部内容基于西安电子科技大学潘蓉老师的《面向对象程序设计》课程记录而成
静态成员
同一个类的不同对象,可以访问相同的静态成员变量、静态成员函数。
与静态成员相对的,非静态成员又被称为实例属性,它们的值是每个对象所特有的。
用static关键字声明的静态成员的属性则是被所有同类对象共有的类属性。
**静态成员不会在每个对象内部占用空间。**其空间在程序编译时进行分配,在整个程序中只存在唯一的地址(独立于任何对象),由所有对象共享访问。生命周期与程序本身相同。
静态成员可以在类不实例化的情况下进行访问。
static int a; // 这么声明就ok了
静态成员的初始化
静态成员只能在类体外初始化。
但是可以在类的函数中(包括构造器等)进行重新赋值等。
class Student {
...
static int stuNumber; // 某类东西的数量,这种东西值得各个对象共享
...
};
...
int Student::stuNumber = 0; // 需要指出类型,不能加 static
静态成员的使用
类的函数可以直接访问,无需指明使用哪一个(即无需作用域说明)
在类外访问,必须使用成员访问运算符(.)或作用域运算符(::)
cout<<Student::stuNumber; // 没有实例也可以用
cout<<stu1.stuNumber; // 这样也行
静态成员可以有权限限制
只有 public 的才可以在类外直接访问
静态成员函数
即,加了 static 声明出来的函数。其可以处理静态数据成员,但不能直接访问非静态成员。
如果想要访问非静态成员,需要通过参数传递方式得到对象名,然后通过对象名访问该对象的动态成员。
没有 this 指针。
// 访问非静态成员
class Student {
static void showInfo(Student stu) {
cout<<a<<stu.b; // 静态的直接引用,非静态的需要加上对象名
}
static int a;
int b;
};
int main() {
Student stu1;
...
Student::showInfo(stu1); // 这样把一个对象传过去就能访问了
...
}
对象指针
对象指针,指向对象所在内存的起始地址。
<类名> *<对象指针名> = &<对象名>; // 声明和初始化赋值。与普通指针相同
<指针名> -> <public成员名>; // 访问
(*<对象指针名>).<public成员名> // 访问
用对象指针作为参数
Student inputInfo(Student *stu) {
string name;
cin>>name;
stu->name = name;
return *stu; // 返回指针所指向的对象
}
动态对象
new <类名>; // 申请成功会返回一个指向对象的指针
new <类名>(<参数列表>); // 自动调用带参构造器
delete <指向对象的指针名>; // 手动释放对应对象的内存空间
Student *stuPtr = new Student;
delete stuPtr;
this指针
该指针是一个隐含指针——隐含在每一个成员函数中(除静态的)——每一个成员函数都有一个。
其指向调用该函数的对象——即,值为当前函数所在对象的起始地址。
这里有个有趣的问题——既然同一个类的所有对象都共享同一份成员函数代码,那么为什么我们调用函数的时候,函数总会找到自己属于哪个对象从而访问其该访问的成员变量呢?这就问题就是通过 this 指针解决的。
对象调用成员函数时,编译系统先将对象地址赋值给 this ,然后再调用。每次成员函数访问成员变量时,本质上都使用了 this ,只不过是没有显式地使用而已(是编译器再编译的时候把对象地址加上去的)。
this 是 const,成员函数不能重新赋值之。
静态成员不能访问 this(显然,static成员 不属于任何的 that )
this 的显式使用
this 指针,返回的是指向当前对象自身的指针。对象用自己的函数返回自己的成员,可以:
this->memberVariable; // 是指针
(*this).memberVariable; // 是对象
// 用 this 来进行拷贝
void Student::copy(const Student & stu) {
if( this != &stu ) { // 检查一下当前在哪个对象里,避免自己拷自己
...
}
}
成员指针
可以使用一个指针绕过对象本身,直接访问该对象的成员。
实现这样功能的指针就叫做成员指针。
指向非静态数据成员的指针
定义方法与普通指针完全相同。
int *p = &time.hour;
指向非静态数据成员函数的指针
普通的指向普通函数的指针:
void (*p)(); // 普通的指向 void 型函数的指针变量
p = func; // func 是一个定义好的函数,这一行用指针指向它
(*p)(); // 通过指针变量调用,等效于 func();
指向public函数的指针:
函数类型名 (类名::*指针变量名)(参数表); // 要表明指针指向哪个类中的成员函数
指针变量名 = &类名::成员函数名; // 指向类中的函数
int (Point::*pGetX)(int a); // 比较一般的写法
void (*p)();
p = &Time::showTime;
(t.*p)(); // 新的调用方法,等效于 t.showTime();
指向静态成员的指针
class Point {
...
static int count;
...
static void GetC() {
...
}
};
int Point::count = 123; // 静态数据成员,类外初始化
int *countPtr = &Point::count; // 指上静态数据成员
void (*gc)() = Point::GetC; // 指上静态成员函数
gc(); // 调用就这样
对象引用
Reference,是某个变量的别名 alias
引用,是直接访问对象。指针,是间接访问
Time myTime;
Time &refTime = myTime;
refTIme.func();
myTime.func(); // 完全等效
引用调用
Student returnS(Student s) {return s;}
Student stu1;
stu1.returnS(stu1); // 到这一行,首先会调用 Student 类的复制构造器。
// 复制构造器将形参 s 初始化为实参 stu1
// 然后,第二次构造复制构造器,以将 return s 的这个返回值对象初始化为 s
// 接下来 returnS 的返回值对象调用析构器,将返回值对象析构。
// 然后形参 s 对象的析构器也要被调用。
// 就这样,啥都没干就调用了两次构造器、调用了两次析构器。成本过高。
而,参数的引用传递可以有效避免值传递带来的高额开销。
Student& returnS(const Student& s) {
return s;
}
这样会直接将引用传递过去,没有任何产生副本造成的额外的开销。
共享数据的保护——常对象
对于需要被共享,且值不能被改变的量,可以设置为常量。
const <数据类型名> <常量名>=<表达式>;
常对象:其数据成员值在该对象生命周期内不能被改变
const <类名> <对象名>(<初始化值>); // 常用这种
<类名> const <对象名>(<初始化值>); // 两种效果相同
const Time t(1, 1, 1); // 其所有的数据都是常量,因此必须被初始化
常对象不能调用普通的成员函数——防止在成员函数中尝试修改常对象数据的值
编译时,编译以一个源程序文件作为单位来进行编译。如果函数的定义和声明、调用不在一起,那么编译器就无法对函数内部结构进行检查,导致错误遗留到链接、运行阶段。因此,编译器干脆就不对函数内部进行检查。
mutable
常对象中, 用 mutable 声明的变量,可以被声明为 const 的成员函数修改。
类的常成员
加 const 声明的变量和函数。
const 变量 只能 通过构造器的参数初始化列表来对常成员进行初始化。
const int Hour;
Time::Time(int h): Hour(h) {}
常成员函数
可以通过常成员函数访问数据成员,但不能够修改值,也不能调用该类的非 const 函数
<数据类型> <函数名> (<参数表>) const; // 这个 const 要写在最后。声明和定义时候都要加
常对象只能调用常成员函数
常对象中的函数 不等于 常成员函数!只有带 const 的才是常函数!
const 还可以用于对重载函数的区分。
class R {
R(int i, int j) {
R1 = i;
R2 = j;
}
void print();
void print() const;
int R1, R2;
};
...
R a(5, 4);
a.print(); // 普通对象,调重载的普通函数
const R b(1, 2); // 声明一个常对象
b.print(); // 常量对象,调重载的常函数
const 指针
指向对象的常指针
这样的指针指向不能再改变,但其所指的对象可以改变。
类名* const 指针变量名 = 对象地址;
指向常对象的指针
常对象只能用 const 型的指针指向。
const 类型名* 指针变量名;
这样声明的指针可以指向常对象,也可以指向普通对象。不能通过指针改变对象的值,但是指针本身的值可以改变(也就是重新指向其他对象)。
这也就是说,可以实现【有保护的使用】
例:
Time t1, t2;
const Time* p=&t1;
(*p).hour=18; // 错,不能通过常指针修改变量
t1.hour = 18; // 对,t1 不是常对象
p = &t2; // 对,指向常对象的指针仍然是指针,可以被重新赋值
常引用
声明引用时使用 const 修饰的引用
常引用所引用的对象不能被更新。
const 数据类型 &引用名
常引用参数:函数中不能改变实参对象的值
void fun(const Time &t);
对象数组
<类名> <数组名>[<下标表达式>];
// 数组建立时,每个元素都是一个独立的对象,也就是说有多少个元素就要创建多少次对象
Student stud[3] = [11, 22, 33]; // 这样填写构造器实参
Student exStud[2] = {Student(11, 'abc'), Student(22, 'def')};
// 构造器有多个参数,需要这样调用构造器
Student exStud[2] = {Student(11, 'abc')};
// 如果构造器有默认参数值,则第二个直接通过默认参数构造
访问:
<数组名>[<下标>].<成员名>
动态对象数组
CPoint* ptr = new CPoint[5]; // 声明并分配空间
...
delete[] ptr; // 释放上面动态申请的内存
对象成员(子对象)
class A {
...
};
class B {
B(const A &a):m_a(a) { // 在这里调用了复制构造器,把 a 复制给 m_a
...
}
/* 或
B(const A &a):m_a(a) {
m_a = a;
...
}
*/
A m_a; // 对象成员。A需要在B前面进行声明
};
有子对象的类在进行初始化时,先调用子对象的构造器,再调用本类的构造器。析构相反:即先析构自己,再析构各个子对象。
如果类没有写自定义构造器,则子对象构造时使用的也是默认构造器。
与对象成员类似,还有一种叫做对象成员数组的东西
类模板
类模板是类的抽象,类是类模板的实例。
类模板,是对一批【仅有数据成员类型不同的】类的抽象。
因为在这个类中,数据类型也成为了参数,故又称“参数化的类”。
类模板实例化出来的类:“模板类”(从模板来的类)
template <class 类型参数> // 类型参数数量不限
class <类模板名> {
...
};
// 例1:
template <class T>
class Compare {
public:
Compare() {
x=0;
y=0;
}
Compare(T a, T b) {
x=a;
y=b;
}
T max() {
return (x>y)?x:y;
}
private:
T x, y;
};
// 例2:
template <class T1, class T2> // 多个类型的参数
class A {
...
};
A<int, double> obj; // 实例化对象。先实例化出对应类型的类,再实例化对象
template <class T=int> // 使用默认参数的类模板
class Array {
...
};
Array<> intArray; // 可以留空