1. 构造函数
class 类名 {类名(行参表){. . .}}
成员函数名和类名一样;
当一个对象被创建是,构造函数会自动调用被执行;
参数来自于构造实参;
构造函数也可以通过参数实现重载;
构造函数不能指定返回值,连 void 都没有;
class Student{
private:
string m_name;
int m_age; // 这里是声明部分,不能初始化,例如:int m_age = 20; 错!
public:
Student(const string &name, int age) // 构造函数给成员变量初始化
{ m_name = name; m_age = age; }
Student(void) // 构造函数重载,无参构造函数
{ m_name = name; m_age = age; }
void eat(const string &food)
{ cout << food << endl; }
};
int main()
{
Student s1 = {"zhangfei", 23}; // ok
Student s2 ("zhangfei", 23); // ok
Student s3 = Student("zhangfei", 23); // ok
Student s4 (); // error,编译报错,编译器把这里的s4当作函数声明
Student s5; // ok,匹配无参构造函数,建立对象s5
Student *s6 = new Student ("zhangfei", 23); // 堆里面创建对象,并把地址赋值给s6
Student -> eat("kaoya");
delete s6;
Student &s7 = *new Student (); // s7是引用,需要值,所以new前面加 *
s7.eat();
delete &s7;
Student ss1[3]; // 这个数组没参数,所以调用无参构造函数
Student ss1[0].eat("miantiao");
Student ss2[3] = {s1, s2}; // 最后一个调用无参构造
Student *ss3 = new Student[3]; // 调用无参
delete[] ss3;
Student *ss4 = new Student[3] = {s1, s2, s3}; // new同时初始化,2011编译器才支持
ss4.eat("kfc");
delete[] ss4;
return 0;
}
成员变量独占内存,成员函数可以被多个成员变量共享;
上面这个程序有 s1 s2 s3 等多个成员变量,但在内存中,只有一个 eat() 函数,共享;
缺省构造函数:
如果一个类没有定义任何构造函数,那么系统就会缺省地为其提供一个无参构造函数;
该构造函数对于基本类型的成员变量不做初始化;
对于类类型的成员变量,调用其相应类型的无参构造函数初始化;
如果已经有构造函数,则系统不再提供默认无参构造函数;
class Student {
public:
string m_name;
int m_age;
}
int main() {
Student s; // ok
return 0;
}
另外:
class Student {
public:
string m_name;
int m_age;
Student(const string &name, int age) // 系统不再提供无参构造
}
int main() {
Student s; // error,编译器报错,无法调用无参构造
return 0;
}
构造函数不能声明为 const,因为 const 构造函数是不必要的;
构造函数的工作是初始化对象,不管对象是否为 const,都要初始化;
2. 对象构造过程:
分配内存 -> 构造成员变量 -> 调用构造函数
构造被调用后,对象就构造完成了;
调用构造函数是构造一个对象最后一步;
只要对象被构造,就会调用构造函数;
那么,构造函数主要目的是为了初始化对象,也就是初始化对象的成员变量;
语法上,构造函数中可以做任何事,一般情况下是作初始化工作;
3. 初始化表
class 类名 {类名(行参表):初始化表{. . .}}
class Student{
public:
string m_name;
int m_age;
student(const string &name) : m_name(name), m_age(age) // 单参构造函数
{ } // 如果在函数体里面又赋值,那么对应的初始化值就无效;一般初始化表和赋值二选一;
};
int main()
{
Student student ("zhangfei");
return 0;
}
这里初始化表如果写为:
student(string m_name = "zhangfei", int m_age = 30) : m_name(m_name), m_age(m_age) {}
首先,这里函数体可以什么都不写;
其次,行参表里声明并初始化局部变量 m_name 和 m_age;
局部变量名和成员变量名一样,初始化表会自动区分;
如果在行参表里面给参数初始化,那么就不需要单独些空参构造函数;
如果在这种情况下还写空参构造函数,那么会编译错误,产生歧义;
4. 下面这种情况下只能用初始化表,而不能用构造函数体赋值:
如果用函数体赋值的办法,那么编译报错,因为先有构造初始化,然后才能赋值;
class A {
public:
A (int a) {}; // A有构造了,所以系统不会分配无参构造
}
class Student {
public:
string m_name;
int m_age;
A m_a; // 如果无下面的初始化表,那么编译器报错,因为这里m_a会调用A的无参构造,但是A没有无参构造
student(const string &name) : m_a(100) // m_a 用有参A构造
{ } // 如果仅仅是在函数体里面赋值,则会报错;
};
如果类中含有常量 const 或者引用型的成员变量,必须通过初始化表对其初始化;
class A{
public:
A():m_a(100) {} // 这里如果在函数体里写成:m_a = 100; 则报错;分析如下:
private:
const m_a; // 有 const,则必须用初始化表初始化;
}
分析:首先,m_a 是常量,m_a = 100; 这种赋值方法是错误的;
然而,成员变量是比需要初始化的,后期赋值又没法达到初始化效果;
那么,只能用初始化表,初始化表是在分配内存空间的时候,就进行初始化;
至于引用必须用初始化表,原因是:
引用在创建的时候,必须初始化,也就是有引用对象,所以只能用初始化表;
在分配内存空间的时候,就进行初始化;
5. 成员变量的初始化顺序仅与其被声明的顺序有关,与初始化表的顺序无关;
class A{
public:
A(char *psz):m_str(psz), m_len(m_str.length()){} // error
private:
size_t m_len;
string m_str;
}
// 当 m_len 调用 m_str.length() 的时候,m_str 还不存在,所以这样写是错误
有两种办法解决上面的问题:
第一是把 private 里面的两个声明变量交换顺序;
第二是降低其耦合度,也就是把 m_str.length() 写成 m_str.length(psz);
6. 一般在正式项目开发中,类的声明和定义都是分开的,在不同的文件里面;
类的声明一般在 .h 头文件里面,需注意:
函数的默认值写在声明处;
函数初始化列表不能写在声明处;
类的实现/定义一般写在 .cpp 文件里,同样需注意:
include 头文件 .h
默认值不可以在这里出现,否则编译错误;
初始化列别写在这里;
所有函数的函数名前都要加上如下格式:
类名 ::函数名(){}
student.h 内容如下:
#ifndef STUDENT_H
#define STUDENT_H
class Student{
public:
Student(const string &name = "", m_age = 10); // 构造函数声明
void print();
private:
string m_name;
int m_age;
};
#endif
可以用下面代码测试运行:
g++ -c student.h
生成:student.h.gch
--------------------------------
student.cpp 内容如下:
#include <iostream>
#include "student.h"
Student::Student(const string &name, m_age) {...}
void Student::print() {...}
可以用下面代码测试运行:
g++ -c student.cpp
生成:student.o
--------------------------------
text.cpp 内容如下:
#include "student.h"
int main () {
Student s;
s.print();
return 0;
}
编译时输入:
g++ text.cpp student.cpp
生成:text.o
或者输入:
g++ text.o student.o
g++ *.o
小小提示 1:我们在编写完每个文件的时候,记得在末尾空一行;
这是由编译器决定的,如果没留,有些编译器会警告,文件末尾没留行;
小小提示 2:如果我们的某一个函数特别小,可以直接在声明里面写出其实现,没必要一定分两个文件;
7. 钟表练习,可以实现计时和显示当前时间功能:
分析:
Clock 属性:时、分、秒;
功能:显示show、滴答走tick;
代码如下:
#include <iostream>
#include <iomanip> // setfill('0') 和 setw(2) 调用
using namespace std;
class Clock{
public:
Clock(bool timer = ture) : m_hour(0), m_min(0), m_sec(0) { // 用于判断用计时器还是显示当前时间,缺省为计时器
if (!timer) {
time_t t = time(NULL); // 总秒数
tm *local = localtime(&t); // 把秒数转换为结构体 tm,里面含有时分秒;
m_hout = local -> tm_hour;
m_min = local -> tm_min;
m_sec = local -> tm_sec;
}
}
void run(void) {
while(1) { // 相当于 for(;;)
show(); // show无须客户看见,所以放到private里面
tick(); // 同上
}
}
private: // 自恰性
int m_hout;
int m_min;
int m_sec;
void show() {
cout << '\r' << setfill('0') // 填充 0
<< setw(2) << m_hour << ':' // 设置为两位
<< setw(2) << m_min << ':'
<< setw(2) << m_sec << flush; // flush刷新缓冲区,强制输出
// printf("\r%02d:%02d:%02d", m_hour, m_min, m_sec);
}
void tick() {
sleep(1); // 不需要任何新的头文件
if(++m_sec == 60) {
m_sec = 0;
if(++m_min == 60) {
m_min = 0;
if(++m_hour == 24) {
m_hour = 0;
}
}
}
/*也可以写为下面代码:
m_sec++; // 不要忘记这行代码
time_t cur = time (0); // 获取当前时间
while(time(0) == cur); // 休息一秒
if(m_sec == 60) { m_sec = 0; m_min++; }
if(m_min == 60) { m_min = 0; m_hour++; }
if(m_hour == 24) { m_hour = 0; }
如果这样写,记得在主函数或初始化表里给三个参数初始化为 0;
*/
}
};
int main() {
Clock clock; // 无参调用计时器
clock.run();
//Clock clock(false); // 调用时钟
//clock.run();
return 0;
}
8. this 指针
class A{
public:
A(void){ cout << this << endl; };
void foo(){ cout << this << endl; };
};
int main() {
A a;
cout << &a << endl;
a.foo();
A *pa = new A;
cout << pa << endl;
pa -> foo();
}
对所有成员变量的访问,通过 this 指针完成;
一般而言,在类的成员函数或构造函数中,this关键字标志一个指针;
对于构造函数而言,this 指向正在被构造的对象;
对于成员函数而言,this 指向调用该函数的对象;
以下初始化方式:
int i = 45;
void *p = &i; // ok, void 可指代任何类型
long *lp = &i; // error,指针类型和对象类型需一致
9. this 指针用途:
1)在类的内部,区分成员变量;当全局,局部等变量名称一样的时候,this 可以区分;
class A{
A(int data){ this -> data = data; } // 等号右边的是行参data
int data; } // 这个data是成员变量
如果是初始化表,则不需要且不能用this:
A(int data):data(data){ } // 编译器自动区分初始化表前面data为成员变量,括号里面的是参数;
2)在成员函数中返回调用对象自身;(retthis.cpp)
A& a() { ... return *this; }
3)在成员函数内部,通过参数向外界传递调用对象自身,以实现对象间的交互;(atgethis.cpp)
C++ 里不允许直接用对象作交叉类;
class A { B m_b; };
class B { A m_a; };
A里有B的成员变量,B里有A的成员变量;
但是引用或指针就可以:
class A { B &m_b; };
class B { A &m_a; };
10. 常函数
如果在一个类的成员函数的参数表后面加上 const 关键字,这个成员函数就被称为常函数;
常函数可以保护成员变量不被修改,增强安全性;
常函数的 this 指针是一个常指针;
在常函数内部无法修改成员变量,除非该变量具有 mutable 属性;
mutable 具有去常功能;
当常函数有多个变量,可以用 mutable 对其中某几个作修改;必须放在成员声明之前;
而且,在常函数内部也无法调用非常函数;(constfuc.cpp)
常函数和非常函数构成重载关系;
class A {
public:
void foo() const
{ m_i = 100; } // error,这里相当于:this -> m_i = 100; 而this是常指针,不允许被赋值
int m_i;
// 如果加上mutable,相当于去常,就可以了,代码如下:
void foo() const
{ m_i = 100; } // ok,这里相当于:const_cast<A*>(this)->m_i = 100;去常;
mutable int m_i;
};
int main()
{
A a;
a.foo();
cout << a.m_i;
}
11. 常对象
拥有 const 属性的对象,对象引用或指针;
常对象只能调用常函数;非常对象调用非常函数;
如果没有非常函数,那么非常对象也可以调用常函数;
同型的常函数和非常函数可以构成重载关系;
class A{
public:
void foo(){} // 和下面常函数构成重载,编译完后,参数表变为 (A *this)
void foo()const // 编译后参数表变为(const A *this),两者参数并不相同,所以构成重载;
{ m_i = 100; } // error,这里相当于:this->m_i = 100;而this是常指针,不允许被赋值
int m_i;
};
int main()
{
A a;
a.foo(); // 调用普通 foo(),如果没有普通 foo(),那么可以调用 const foo();
const A &r = a;
r.foo(); // 调用常 foo()
cout << r.m_i;
}
12 const 函数名 (const 参数表) const {...}
第一个 const 定义返回值;
第二个 const 定义参数;
第三个 const 定义 this 指针;
全局函数没有 const 型 this 指针;
13. 析构函数
class 类名 {~类名(void){析构函数体}}
不返回任何值,也没有任何参数;
当一个对象被销毁的时候,自动执行析构函数;
局部对象离开作用域时,被销毁;
堆对象被 delete 时,被销毁;
class Double {
public:
Double(double data) : m_data(new Double(data)) {}
void print() const { cout << *m_data; };
~Double() { delete m_data; } // 析构
private:
double *m_data; // 这个m_data属于基本类型变量,所以要专门写析构函数
};
int main() {
Double d(3.14);
d.print(); // 这条语句执行完了,就执行析构
Double *d1 = new Double(3.14);
delete d1; // 堆对象见到delete就执行析构;
return 0;
}
14. 缺省析构:
如果一个类没定义任何析构函数,那么系统会提供一个缺省析构函数;
这个缺省的析构函数对基本类型的成员变量什么也不做;
对类类型的成员变量,会调用相应类型的析构函数;
15. 一般情况下,在析构函数中,释放各种动态分配的资源;
并不是每个类都需要析构函数;
一般的,如果一个类的成员是按值存储,则不需要析构函数;
一个类可以定义多个构造函数,但是析构函数只有一个,它将被应用在类的所有对象上;
一个对象的析构函数只调用一次,但在语法上可以多次调用;
析构函数也不局限在释放资源上;
可以执行编写人员希望在类最后执行的程序;
构造(加载顺序):基类--成员--子类;
析构(释放顺序):子类--成员--基类;
16. 自学补充笔记:
1)尽可能使用 const
const* --- 被指物是常量;
*const --- 指针自身是常量;
*const* --- 被指物和指针都是常量;
2)const 可以施加于任何作用域内的任何对象、参数、函数等,可以帮助编译器侦测出错误用法;
3)当 const 和 non-const 成员函数有着实质等价时,令 non-const 调用 const 版本可以避免代码重复;