- 对象表现的特征是:属性(数据),行为(处理)
- 类:相同属性和行为对象的抽象。
**面向对象的特征:**抽象,封装,继承,多态
面向对象的关键要素:对象;属性;方法;消息/服务。
- 类的定义
// 例:时钟类的定义(和结构类似,也可以不起名,但无实用价值)
class Clock
{
public: // 外部接口,公有成员函数,外面可以调用的部分
void SetTime(int NewH = 0, int NewM = 0, int NewS = 0);
void ShowTime();
private: // 私有数据成员,只能类里自己用
int Hour,Minute,Second;
};
//补充:protect,保护型,在私有和公有之间,可被派生(继承)类使用
//public 和 private没有先后顺序,甚至可以穿插定义
- 对象
Clock a1; //创建对象 a1.Showtime(); //引用,类似结构(c++对结构体进行了功能扩充,其实和类很像了)
- 类的成员函数(方法)可以在外部定义(记得先声明)
void Clock::SetTime(int newH = 0,int newM = 0,int newS = 0){
Hour = newH;
Minute = newM;
Second = news;
}
//返回值 类名::函数名(参数表,可以带默认形参值){函数体}
- 内联函数:好处是快
原理:普通函数的调用,历经分配内存,传递参数,处理返回值,回收内存,返回原程序地址等操作,都需要时间。内联函数相当于智能地将函数变成顺序操作整合到此处,不需要额外的工作。程序员多敲个单词,对人和机器都好
由此可见,内联只适合层次不深的简单函数。理论上是个函数都可以内联。
但是使用 inline 会自动检查,对复杂函数,若无法内联,则忽略该关键字
c/c++基础课上的程序相对简单,暂不需要这种操作,所以没讲
//示例:外部声明的函数前面加上 inline
inline void Test::output( )
{
cout << “正确的" << endl;
}
- 以学过的内容举例:string 对象里封装了具体字符串的地址和其他信息。相比 char[] 不会有越界的问题,需要包含
string操作简单,可以定义string数组,可以用cin,cout。可以直接赋值不用考虑越界,可以和 " " 包裹的字符串进行计算。可以用 + 连接,可以比较(字典序)。还可以用 [下标] 定位和操作某个字符。此外还有各种函数(略)
但是熟练使用指针和字符数组对程序员来说始终是很酷的事情
构造函数(基础)
和类名同名的成员函数(定义是如此),一般用来进行对象初始化。无返回值,带参数可重载(同函数名对不同参数类型执行不同操作)
若未定义构造函数,会自动生成一个空的构造函数
**个人理解:**类特有的内容,使用类名时既创造对象,又执行了构造函数。之后就不需要关注这个函数了。
//宣总(老师)的示例,构造函数重载
class Test{
private:
public:
Test();
Test(int n);
Test(string str);
};
int main(void){
string s1 = "wang",s2 = "zhang";
Test x; //生成一个名叫x的对象,同时执行Test()
Test y(15); //生成同时执行Test(int n)
Test array1[2] = {s1,s2}; //生成同时分别执行2次Test(string str)
}
对重载函数的补充
- 函数重载在底层视为两个不同的函数,仅仅是重名了而已。
-
函数重载不能出现矛盾/歧义。
-
调用时传入的参数类型,没有找到对应的函数,则调用参数个数相同的函数。此时若有多个函数满足,则歧义,无法执行
-
参数声明时已赋默认值,此时传入空参数也可以调用,但也可能出现歧义。
-
-
函数有多个参数且有意义时,调用时可以从右往左省略参数。
构造函数的一种重载,参数是同类对象的引用。一般用于数据复制式的初始化等(摘自宣总的 ppt)
//已知类 A 有 private 数据 n
A::A(A& a);
{
n = a.n;
}
初值列——构造函数特有(炫酷高效)的用法
//示例(截取部分)
class Point
{
public:
Point (double a = 0, double b = 0)
:re (r), im (i) //这一行就叫初值列
{ }
private:
double x, y;
}
Singleton——单例模式**优点:**一般的初始化,变量事实上经历了创建和赋值两步;而初值列直接在创建同时赋值,只用了一步
很多类直接在外部创建对象,构造函数放在public里。
但对于一些特殊的类,为了防止外部创建对象,构造函数需要放在private里。同时配套特有的生成对象方式 ↓
class A {
public:
static A& getInstance(); //访问类的public函数,返回静态对象的引用。静态函数操作静态成员
setup() { ... }
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}
A::getInstance().setup(); //通过函数返回的对象调用其他成员函数,static 使得访问的对象固定为唯一的对象a
常量成员函数(仅限非静态函数)
- 在参数和函数体间加 const ,可以使函数不能修改成员变量,提高安全性
通俗来讲就是加上只读标签,凡是调用此函数,都不能修改数据。(一般只要在只读场合下,为了保险都需加上这个标记x)
特别的: 当对象或成员为 const 类型时,函数必须标为 const ,即使是读取功能的函数,如果没有标,直接编译错误。
例:int a() {return a;} 在 {const A p1(1, 2); cout << p1.a();} 下报错
参数传递对于小数据,可以传值;但对于大结构数据,需要传引用节省开销。
c++ 的引用与 c 的指针,在底层是类似甚至相同的
区别大概是——指针是实体传送门,是看得见摸得着的变量。而引用是绑定,和八云紫(老太)的隙间一样,看得见摸不着。仅做解释
不说了,我听到电车撞过来的声音了——
引用在定义时必须赋予变量初始化,之后不可改变指向,可以直接替代变量名,也可作函数参数。
引用的类型——在&前面加上变量类型,如 int &
指针也是变量,可以被引用。引用不是变量,但可以拿引用初始化引用,共同指向一个变量
函数可以以引用为返回值,返回一个定义的变量(不是函数内的局部变量)的引用并赋给一个引用(用处不大)
- 引用可以直接接触并修改外界数据,在修改数据的时候方便,在不想修改时反而不安全。
所以凡是不希望修改对象,都在参数前加 const 。之后不能通过此引用修改,但仍可通过其他途径修改。
指针和引用另外,const 引用可以用 const 或非 const 的变量/引用初始化,而非 const 引用不能用 const 的数据初始化
-
传递参数时,引用比指针更直观更简洁。
-
指针有时候会在函数内改变指向,干坏事,因为是形参,不会真的改变指针,从而隐藏行踪,听起来很不安全。
而引用直接不能改变指向,源头杜绝问题。
-
如果想特地改变指针指向,可以使用指针的指针 / 指针的引用,此时指针本身不再是形参,而是可以被函数修改的外部数据。
返回值传递指针的引用一般这样写: int * & a 。其中 int* 是变量,所以表示对 int* 的引用
为了高效,在合理情况下,函数返回值也建议使用引用承接。
//举个例子:标准库中定义的复数类,如何重载 += 运算
inline complex& __doapl(complex* ths, const complex& r) {
ths->re += r.re;
ths->im += r.im;
return *ths;
} //重载标准库的__doapl函数,将后者的数据加到前者中,返回由外部传入的指针
inline complex& complex::operator += (const complex& r) {
return __doapl(this, r);
}
//在类中重载+=。this代表当前对象的指针,是隐藏参数。不能参与声明,只能被调用。函数返回值为complex对象,适用于连加表达式
//__doapl()不仅可用于连加,还可以放进其他函数,这种将运算分内外层实现的操作,体现了标准库函数设计的泛用性和可移植性
- 在只需要数值结果的时候,可以返回临时值而非引用,比如 + 运算返回的是一个结果值
- 相比参数,返回值不是能随便引用的。
函数注定是局部空间,外界的数据返回时可全身而退。但函数内创造的内容,随着函数消亡,其引用也会失效。
相当于给别人拷游戏只拷个快捷方式就跑路的屑事,回头顺着引用来找的时候却已物是人非
学个c++,却隐约嗅到刀子的气息
友元friend
背景:类是封装的,其中 private 一般不给外人看。但朋友可不是外人。
在 A 类内声明一个前面加了 friend 的函数 / 类的名字。此后 A 可无视权限调用此函数 / 类(如引用该类的成员变量)
另外,引用调用成员直接用 “.” 而非 “->”
友元的类型-
可以是一个普通的外界函数(函数声明前面加了个 friend )
-
可以是一个类下的某个函数( friend 类名::函数 )。必须先定义该类,再声明友元函数)
-
可以是一个类( friend class 类名 。友元类可以先声明,再定义。该类下的所有函数也都能被访问。
让我访问!!!)
同一个类下的所有对象都是友元。如果想通过引用访问该类下其他对象的成员,他们随时向你敞开大门(家是永远的精神港湾)
友元并没有说是相互的,所以经常有单向的 friend 声明(舔狗行为)
操作符重载
- 成员函数:
//还是上面那个标准库 += 的例子
inline complex& complex::operator += (const complex& r) {
return __doapl(this, r);
} //如果只有一层计算可以返回void,如果有一连串+=计算,可以返回引用,运行效率很高
- 非成员函数:
//标准库中复数的 + 运算
inline complex operator + (const complex& x, const complex& y)
{
return complex (real (x) + real (y), imag (x) + imag (y));//real()和imag()返回实部和虚部
}
//这就是一个不能返回引用的例子
c2 = c1 + c2;
//因为 + 不需要修改某个对象,因此逻辑同 += 有一定差异
临时对象
直接使用类名带参数创建的对象,有效范围仅限当前行。经常用于中转数据
- 举个(不太严谨的)栗子:
c++ 特有的一种语法—— int(a) 可将 a 转换为 int 型参与计算
形式上似乎是一个 int 类的构造函数,但实际上 c++ 不是完全面向对象的语言,只是对基本类型实现了一些类面向对象的方法。
但从面向对象思路出发, int(a) 没有给“对象”命名,所以到下一行就消亡了。
//取自标准库,+ 的另一种重载,表示正号而不是加法
inline complex operator + (const complex& x){
return x;
}
cout << +c1;
-
有人到这就要问了:为什么不返回引用呢?
-
回答是:完全可以返回引用。标准库只是一套代码示范,不是金科玉律
操作符的区别就是函数前面多了一个 operator ,但有 operator 的不一定是操作符
//对流的重载 #includeostream& operator << (ostream& os, const complex& x) { return os << '(' << real (x) << ',' << imag (x) << ')'; } //返回ostream &,适用于连续的 << 输出
分析:
- << 一般不重载为成员函数,因为在 cout 是 ostream 类的对象,<< 在标准库中是 ostream 的成员函数,所以一般 cout 为调用 << 的主体,写在 << 前面,而被输出对象写在后面
- 如果 << 重载为成员函数,此时被输出的类为调用主体,写在 << 前面,颠倒了位置,输出流程会变得很麻烦,输入同理。
- 因为 << 在左移输出过程中不断更改着 cout 对象的状态,所以该方法不加 const 标记
以上是个人理解,欢迎纠正
实例分析
探究
class String
{
public:
String(const char* cstr = 0); //构造函数
String(const String& str); //拷贝构造函数
String& operator=(const String& str); //操作符重载,给予“赋值”功能
~String(); //析构函数
char* get_c_str() const { return m_data; } //(内联的)功能函数
private:
char* m_data; //字符串首地址作为成员变量
};
inline String::String(const char* cstr = 0)
{
if(cstr){ //PS:const在*前面是字符串常量指针,不能修改字符串。若const在*后面则是指针常量,不能修改指针
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}else{ //无字符串输入
m_data = new char[1];
*m_data = ' '; //空字符串用' '标记末尾
}
} //以上仅为一种初始化方式
String s1("hello");
//析构函数在对象消亡时用于清理,例如
inline String::~String()
{
delete [] m_data; //指针会自动消亡,但指针分配的内存需要手动free
}
- 补充: c++ 动态内存分配之 new 和 delete
String *p = new String(); delete p; //delete 时会自动调用析构函数
对于单个数据,使用new和delete
对多个数据,则用 new type [n] 和delete [ ]。
**注:**delete的机制,对于基本数据类型的数组,不需要加 [ ]也可以一次性删除
——但自定义数据类型,特别是内部有指针内存分配的结构,要加 [ ] 指明。,否则 delete 的流程是:检测到第一个指针,释放第一个指针,同时删除所有指针,导致后续指针内存泄漏。
总之都加上肯定没问题
- 补充: 关于拷贝构造和赋值重载
拷贝构造:实现复制的功能
赋值重载:重载的目的是——
如果不重载,默认直接将指针的值进行赋值,两个对象指向同一个字符串,称为浅拷贝;
重载后,先清理内存,再分配内存,可以实现另外分配内存的深拷贝,还能防止原先有字符串的指针,因为指向了另一块内存而导致原先字符串内存泄漏。
//String的拷贝构造
inline String::String(const String& str)
{
m_data = new char[strlen(str.m_data) + 1]; //先拷内存
strcpy(m_data, str.m_data); //再拷字符串
} //只用于初始化
String s2(s1);
- 运算符重载
inline String& String::operator=(const String& str)
{
if (this == &str)
return *this; //检测自赋值情况,防止字符串被清空
delete[] m_data; //打扫门户
m_data = new char[ strlen(str.m_data) + 1 ]; //重开内存
strcpy(m_data, str.m_data); //存放数据
return *this;
}
- 字符串输出
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str(); //重载 << 实现字符串的输出,调用类内获取字符串的函数
return os;
}
cout << s1;



