写了四年C++但面向对象啥都不会的我震惊了。

面向对象编程速成

一。C++ 过渡

1. 引用:

int &r = n,表示 r 是 n 的一个引用,即 r 与 n 等价,修改其一两者都会改变。

  • 引用初始化时指向某个确定的变量且不可更改;
  • *a 与 &x 地址的调用与引用相当;
  • 函数的引用等价其返回值。

Example: swap(int &a, int &b)

2. 常量关键字

const int n = 300const int *p = &x 表示常量或常量指针。

  • 常量指针不能修改所指向的内容;
  • 普通的指针不能赋常量指针的值(否则与第一条矛盾)

特别地, 若函数返回值是 int&,即返回的是返回的地址所指的变量

3. 内存动态分配

  • P = new T(P 是 类型 T 的指针*),开辟 sizeof(T) 大小的内存且将起始地址赋给 P;
  • P = new T[N](数组),开辟 sizeof(T)*N 的空间且将起始地址赋给 P;

可利用 *P = ?? 来修改开辟这片空间的内容。

  • delete Pdelete P[]释放空间,其中 P 必须为动态开辟的内存;

4. 内联函数和重载

  • inline 将参数入栈后返回调用地址,减小函数调用开销。

Example: inline int read() {}

  • 函数重载:多个名字相同而参数个数或类型不同,可以使程序直观;

Example :

int max(int a, int b, int c)
{
if (a>b && a>c) return a;
else return max(b,c); // 这里的 max 是库里的函数
}

函数缺省 调用函数时可以让最右侧连续的参数缺省,提高程序可扩展性。

二。类与对象基础

在结构化程序设计中 程序 = 数据结构 + 算法,却不能对具有相同事物(变量或函数)进行易于整理的归纳;

而在面向对象程序设计里,一切都是围绕着 对象 展开的(虽然我没有),将数据结构和对应的函数捆绑,形成一个 ,使得数据结构和算法与对象呈现紧密联系,我们称其为 封装

Class 定义

C++ 中用 Class 定义一个类,包含了不同的成员变量和成员函数,称其为一个对象。我们可以用 对象名.成员名 对其进行调用。在类的外部写成员函数需要加上类的名字 类型名 对象名:: 函数名()

类成员可访问范围

  • Private:私有成员,只能在成员函数内部访问;
  • Public: 公有成员,可以在任何位置中使用;
  • 若未定义则默认为 Private.

类成员函数内部,可以访问到当前对象的全部属性及函数;而在函数之外的地方只能访问其公有成员。

设置私有成员的机制称为 隐藏,使得只能通过成员函数来访问私有成员,以后修改成员变量类型等属性后只需修改成员函数而不用修改所有语句。

构造函数

构造函数可以对对象进行初始化,如给成员变量赋初值,对象生成时构造函数自动被调用(若没写则编译器会生成默认的构造函数)。构造函数无返回值(不用写 return)。

ExampleExample :设计学生姓名年龄成绩的档案。

class student {
private :
char *name;
int age;
double score;
public :
student(); // 两个重载的构造函数
student(char *Name, int Age, double Score);
};

student::student() { name = "", age = 0, score = 0.0; }
student::student(char *Name, int Age, double Score) {
name = Name, age = Age, score = Score;
}

int main()
{
student stu1("TZ", 114, 51.4);
student *stu2 = new student;
}

复制构造函数

对同类对象引用的构造函数,即为复制构造函数,形如 X::X(const X &) {},除了普通的 A x1 = x2 这类初始化:应注意到这里的参数应该是引用,否则会递归地调用复制构造函数。

  • 若某个函数有一个参数是类 A 的对象,则该函数被调用时,类 A 的复制构造函数也将被调用,如:
class A {
public :
A() {}
A(A &a) { cout<<"没有赋值捏\n"; }
};
voud Func(A a1) { ... }
int main() {
A a2; Func(a2);
//此时调用了 Func 函数,其参数 a1 会调用复制函数,赋值 a2.
//但事实上赋值构造函数并非仅可用来复制,比如本例。
return 0;
}
  • 若函数的返回值是类 A 的对象,则返回时 A 的复制构造函数会被调用.
class A {
public:
int v;
A(int n) { v=n; }
A(const A &a) { v = a.v; cout <<"复制了捏\n"; }
};

A func() { A b(4); return b; }
int main() {
cout << func().v << '\n'; // 函数临时生成一个类 A 对象 (4)
return 0;
}

同时注意到: A c2=c1A c2(c1) 有时候并非等价。

类型转换构造函数

只有一个参数且非复制构造函数,称为类型转换构造函数,将某一类型转换为对象 A,如:

class A {
public :
double r, i;
A(int x) { r = x, i = 0; } //类型转换..
A(int x, int y) { r = x, i = y; }
};
int main()
{
A c1(7,8); A c2 = 12; // 调用类型转换..
c1 = 9; // 编译器临时生成一个 (9,0) 的A对象,并将其赋值给 c1
}

析构函数

形如 ~ A() {} 的成员函数称为析构函数

当对象消亡时(如return 0, delete)自动调用析构函数,可以帮助对象做一系列善后工作,如释放分配空间等。一个类至多有一个析构函数。

特别的,new 一个类 A 的数组,其每个元素都是一个对象,则会调用多次析构函数.

三。类与对象提高

静态成员

加了 static 关键字的就是静态成员,被所有该类的对象所有共享。因此,静态成员不需要通过某一特定对象访问,可以直接这样写:

class {
...
static void Print() { ... }
}

...

{
A :: Print(); // 可以这样直接调用
}

设置静态成员可以将和某些类紧密相关的变量联系在一起,防止其他类对其访问,便于维护。但是在定义类时应至少对静态成员变量进行一次初始化,否则不可被链接。

值得注意的是,在静态成员函数中,不能访问非静态成员变量/函数(因为静态并不为某一对象独有);

C++ 成员函数本质

在 C++ 对象中的一段代码:

void A::set(int p) { price = p; }

在 C 中会被翻译成一个多带了一个指向成员对象的 this 指针的全局函数,即:

void A(struct A *this, int p) { this->price = p; }

在 C++ 中,非静态成员函数也可以直接使用 this 来代表指向该函数作用对象的指针:

class Complex {
public:
double real, imag;
complex(double r,double i) : real(r), imag(i) {}
void print() { ... }
Complex Add () {
this->real++, this->print();
return *this; //可直接返回该对象
}
}

成员对象与封闭类

若一个类的成员是其他类的对象,则称其为 成员对象;拥有成员对象的类称为 封闭类,如:

class Tyre{  // 描述轮胎的一个类
private :
int radius;
int width;
public :
Tyre(int r,int w) : radius(r), width(w) {}
};

class Car {
private :
int price;
Tyre ty; // 此时 ty 即为一个成员变量
public :
Car(int p, int rr, int ww) : price(p), Tyre(rr, ww) {}
// 此时若未定义构造函数,则默认构造函数会出错
// Tyre 有其构造函数需要提供参数,编译器不确定
}

封闭类对象生成时,先执行所有成员对象的构造函数,再执行封闭类的构造函数;成员对象的构造函数 先说明先构造,先构造后析构

常量对象

如果不希望某个对象的值被改变,可以在定义对象时在前面加上 const 关键字。

class A{ int val; };
const A obj; //常量对象定义

常量对象不可修改,不可调用非常量成员函数。

成员函数说明后也可以加 const 变为常量成员函数,执行期间不能修改对象(指修改成员变量的值,或者调用同类的非常量成员函数)。此外,若有两个成员函数拥有相同的名字和参数表,但有一个是常量,那这属于重载关系。

在函数调用对象作为参数时,生成函数需要调用复制构造函数,效率较低;我们可以通过调用常引用对象,即减少时间浪费,也防止无意中更改实参:

class A {...};
void func(const A &o) {...}

友元

定义 友元函数友元类 可以访问该类的私有成员:

class Car;
class Drive {
public :
void Modify(Car * pcar) { pcar->price += 1000; }
//可以调用Car类的私有成员;
};

class Car {
private :
int price;
friend int MostExpens( Car cars[], int tot);
friend void Drive::Modify(Car pcar) ;
};

int MostExpens(Car cars[], int tot)
{
int mx = 0;
for (int i=0;i<tot;i++)
mx = max(mx, cars[i].price); // 同上;
return mx;
}

友元不能被传递或继承。

四。运算符重载

重载运算符可以使得C++提供的运算符适用范围得到扩展,使其作用于对象。同一运算符,对于不同类型的操作数,其发生的行为也不同。

对于运算符重载的实质是将运算符函数进行重载,可以重载为成员函数也可以是全局函数。运算符被多次重载时,可以根据实参类型觉得调用哪个运算符。

可以这样写:

class Complex {
public:
double real, imag;
complex(double r,double i) : real(r), imag(i) {}
void print() { ... }
Complex operator-(const Complex &c);
}

// 一个重载加法的全局函数
Complex operator+(const Complex &a, const Complex &b)
{
return Complex(a.real+b.real, a.imag+b.imag);
// 产生一个临时对象
}

// 重载为成员函数,参数个数为运算符目数减一
Complex Complex::operator-(const Complex &c) {
return Complex(real-c.real, imag-c.imag);
// 返回一个临时对象

}

int main()
{
Complex a(4,4), b(1,1);
Complex c = a+b; c.print();
(a-b).print(); // 草,这个可以直接调用,好强;
//等价于 a.operator-(b)
}
  1. 重载 赋值运算符

首先应注意到,直接用等号赋值是一种 浅拷贝,实质上是直接将指针指向后者的地址(即赋值成员变量的地址)。这不仅会导致原来分配的内存 delete 不掉,还会导致修改 s2 而 s1 也变的尴尬情况。

因此我们需要手动模拟一种 深拷贝,即仅修改其内容的赋值语句。

class String {
private : char *str;
public:
String ():str(new char[1]) { str[0] = ''; }
const char *c_str() { return str; }
String & operator=( const char &s) { // 深拷贝
delete []str; //将原来的内容 delete 掉
str = new char[strlen(s)+1]; srtcpy(str, s);
// 分配足够的内存并将 s 拷过来。
return *this;
}
~String() { delete []str; }
}
int main()
{
String s; s = "2";
String S0 = "1"; // 出错,没有设定构造函数
String s1 = s; // 不合适,本质上是指针 s1 指向 s.
// 直接赋值地址被称为浅拷贝。
}

赋值运算符的类型选取 String & 的原因是,赋值运算本质上返回的是左边元素的一个引用。如 (a=b)=c,返回 a 的引用。

  1. STL 中 vector 的实现

利用运算符重载等功能,可实现一个可变长数组。

class Carray {
private:
int sz; int *ptr;
//指向动态分配的数组
public:
Carray(int s=0) : size(s) { //构造函数
if (s==0) ptr = NULL;
else ptr = new int[s];
// 若初始化有元素,则应该开辟一系列空间
}

Carray(Carray &a) {// 复制构造函数
if (!a.ptr) { ptr=NULL, sz=0; return; }
ptr = new int[a.sz];
memcpy(ptr, a.ptr, a.sz<<2);
sz = a.sz; //进行深拷贝
}

~Carray() {
if (ptr) delete []ptr;
}

Carray & operator=(const Carray &a) {
// 同理应进行深拷贝
if (ptr==a.ptr) return *this; // 应付a=a的情况
if (a.ptr==NULL) {
if (ptr) delete []ptr;
ptr = NULL, sz=0; return *this;
}
if (ptr) delete []ptr;
ptr = new int[a.sz];
memcpy(ptr, a.ptr, a.sz<<2), sz = a.sz;
return *this;
}

void push_back(int v) {
if (ptr) {
int *tmp = new int[sz+1]; // 多分配一个空间给 \0
memcpy(tmp, ptr, sz<<2); delete []ptr;
ptr = tmp;
}
else ptr = new int[1];
ptr[sz++] = v;
}

int length() { return sz; }

int & Carray::operator[] (int i)
{ return ptr[i]; }
// 重载 [],使得可直接访问内部数组的元素
// 非引用的函数返回值不可作为左值使用。
}
  1. 流插入运算符的重载

代码里 cout << 5 << "hello" 这种代码之所以可以运行,是因为在 iostream 库中定义的 ostream 类对象对 左移<< 操作进行了重载。即可以写作:

friend ostream& operator<<(ostream &os, const Point& p) {
os << p.x << "," << p.y;
return os;
}
friend istream& operator>>(istream& is, Point& p) {
int x, y;
is >> x >> y, p.x = x, p.y = y;
return is;
}

如果我们想要使流插入对自定义类同样适用,则需要对其重载,比如:

// 内置不进ostream类,重载成全局函数
ostream & operator<<(ostream &o, const A &s) {
o << s.x; return o;
}
  1. 类型转换运算符的重载

强制类型转换本质上是一个函数,那我们就可以对该函数重载。

class Complex{
double real, imag;
public:
Complex(double r=0, double i=0) : real(r), imag(i) {};
operator double() { return real; }
};

int main()
{
Complex c(1.1, 4.1);
cout << (double)c << endl;
double n = 2+c; // 隐式类型转换
cout << n << endl;
}
  1. 自增自减运算符的重载
    前置运算符作为一元运算符重载,而后置运算符作为二元运算符重载;
T &operator++();  // 前置,返回一个T的引用
T operator++(int); // 后置,返回一个T类型

T& T::operator++() { ++x; return *this; }
T T::operator++(int k) {
T tmp(*this); n++; return tmp;
// 返回修改前的对象
}

之所以前置是它的引用,因为 C 语言内部本身如此,这样搞符合原生态C语言特性(比方说 (++a) = 1 这句,最后得到 a 的值为 1.

后置运算符返回的是临时变量,因此 (a++) = 1 没啥影响。

由于前置运算是直接引用,没有产生新的对象,因此会比后置运算快一点点(如果在 STL 中抓一个迭代器做循环,应该写成 for (it = a.begin(); it != a.end(); ++it)