1.C++面向对象特性
1.1C++面向对象的三大特性为:封装、继承、多态 (万物皆对象)
2. 封装
2.1 封装的意义:
其一,将属性和行为作为一个整体,表现生活中的事物。
创建类别忘了"{}“后有个”;",其实可以理解为类与结构体类似。
类中的属性与行为统一称为成员,属性称为成员变量、成员属性。行为称为成员函数、成员方法
class Student
{
public:
string m_name;
string m_number;
void Set(string name,string number);
void Show();
};
void Student::Show()
{
cout<<"学生姓名:"<<Student::m_name<<endl;
cout<<"学生学号:"<<Student::m_number<<endl;
}
void Student::Set(string name, string number)
{
m_name = name;
m_number = number;
}
int main()
{
Student stu;
stu.m_name = "张三";
stu.m_number = "11111";
stu.Show();
cout<<endl;
Student stu1;
stu1.Set("李四","22222");
stu1.Show();
return 0;
}
补充 C++中“->”和“.和“::”的区别:
”->“用于结构体指针调用成员使用 (详情见《C++笔记一》8.3),this指针也是用箭头,因为类与结构体本来就想通。
“::”是域作用符,是各种域性质的实体(比如类(不是对象)、名字空间等)调用其成员专用的。
“.”是成员作用符,是对象专用的。
补充:类内定义的成员函数相当于默认为inline
其二,给属性与行为加权限控制。
权限有三:公有(public)、保护(protected)、私有(private)
共有:类内可以访问、类外也可以访问
保护:类内可以访问、类外不可以访问、子类可以访问父类保护内容
私有:类内可以访问、类外不可以访问、子类不可以访问父类私有内容
举例:
class Student
{
private:
string m_name;
protected:
string m_number;
public:
void Set(string name,string number);
void Show();
};
void Student::Show()
{
cout<<"学生姓名:"<<Student::m_name<<endl;
cout<<"学生学号:"<<Student::m_number<<endl;
}
void Student::Set(string name, string number)
{
m_name = name; //类内可以使用私有成员变量
m_number = number; //类内可以使用保护成员变量
}
int main()
{
Student stu;
// stu.m_name = "张三"; //报错,因为在类外使用私有成员变量
// stu.m_number = "11111"; //报错,因为在类外使用保护成员变量
stu.Show();
cout<<endl;
Student stu1;
stu1.Set("李四","22222");
stu1.Show();
return 0;
}
2.2 class 与 struct区别:
其实我们也可以使用struct去写类,在C++中class与struct唯一区别在于默认访问权限不同
区别:
struct: 默认权限为共有
class:默认权限为私有
class A
{
int m_a; //默认私有 [Error] 'int A::m_a' is private
};
struct B
{
int m_b; //默认公有
};
int main()
{
A a;
//a.m_a; //报错
B b;
b.m_b;
return 0;
}
2.3成员属性设置为私有
优点:其一,把成员属性设置为私有,自己可以控制读写权限。其二,对于写权限,我们可以检测数据的有效性。
对于第一点自己控制读写权限,当我们把成员属性设置为私有后,通过给出不同权限的接口来实现,比如:想把成员属性变成只读,那么只提供一个公有的读函数就行。
对于第二点通过接口,在接口中设置一个数据限制就可以了,每一个写数据都要通过该接口,而接口中可以通过判断等设置一个数据限制(有效性验证)
实例:
class Cube
{
private:
int m_L;
int m_W;
int m_H;
public:
void set(int l, int w, int h);
int getL();
int getW();
int getH();
int S();
int V();
bool isSame(Cube &c); //判断两个立方体是否相等
};
void Cube::set(int l, int w, int h)
{
m_L = l;
m_W = w;
m_H = h;
}
int Cube::getL()
{
return m_L;
}
int Cube::getH()
{
return m_H;
}
int Cube::getW()
{
return m_W;
}
int Cube::S()
{
return 2 * (m_L*m_H+m_L*m_W+m_W*m_H);
}
int Cube::V()
{
return m_L * m_W*m_H;
}
//成员函数判断两立方体是否相等
bool Cube::isSame(Cube &c)
{
if (m_L == c.getL() && m_H == c.getH() && m_W == c.getW())
return true;
else
return false;
}
//全局函数判断两立方体是否相等
bool Same(Cube &c1, Cube &c2) //使用引用好处,见《C++笔记二》
{
if (c1.getL() == c2.getL() && c1.getH() == c2.getH() && c1.getW() == c2.getW())
return true;
else
return false;
}
int main()
{
Cube cube;
cube.set(1, 2, 3);
cout << "第一个立方体表面积为:" << cube.S() << endl;
cout << "第一个立方体体积为:" << cube.V() << endl;
Cube cube2;
cube2.set(1, 1, 1);
bool ret1 = cube.isSame(cube2);
bool ret2 = Same(cube, cube2);
if (ret1)
cout << "成员函数判断两立方体相同!" << endl;
else
cout << "成员函数判断两立方体不同!" << endl;
if (ret2)
cout << "全局函数判断两立方体相同!" << endl;
else
cout << "全局函数判断两立方体不同!" << endl;
return 0;
}
3.对象初始化与清理(构造函数与析构函数)
3.1 构造函数用于对象初始化,析构函数用于清理对象,其语法:
构造函数 :类名(){}
1>没有返回值,也不写void
2>函数名称与类名相同
3>构造函数可以有参数。因此可以发生重载,允许有默认参数
4>程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
析构函数:~类名(){}
1>析构函数,没有返回值,也不写void
2>函数名称与类名相同,名称前加符号“~”
3>析构函数不可以有参数,且不可以重载
4>程序在对象销毁前会自动调用析构,无需手动调用,而且只会调用一次
注意:构造函数时可以重载的,而重载最好不要和默认参数一起用,所以构造函数也最好只用重载 或者只用默认参数。如果非要用到默认参数还有一种方法,《C++primer》中提到使用类内初始值不失一种好选择,使用类内初始值可以搭配构造函数的重载。但如果编译器不支持类内初始值那么在构造函数中最好把所有成员变量都进行初始化。
3.2构造函数的两种分类、三种调用
两种分类方式:
按照参数分:有参构造和无参构造(系统默认的是无参构造)
按照类型分:普通构造和拷贝构造
三种调用方式:括号法、显示法、隐式转换法
注意:构造函数一般都是public的,也有private的,例如singleton设计模式,设置成private后不允许类外创建实例,保证一个类只能有一个实例,并提供一个全局唯一的访问点。仅有一个实例:通过类的静态成员变量来体现。提供访问它的全局访问点:访问静态成员变量的静态成员函数来体现。(具体的等后面学习设计模式之类的再了解学习,现在只用知道基本上都是公有,私有也不会报错且有特殊用处)
拷贝构造函数:是用同一类的不同对象去实例化对象。拷贝构造函数的参数列表中的参数必须为引用类型,且一般都是const,因为const可以防止误操作,避免实参被改变。
无参构造调用不要加(),不然编译器认为是一个函数的声明,就不会创建对象。 eg:有个class A,写A a();便不是调用无参构造。
class A
{
private:
int m_a;
public:
void set(int a);
int get();
A();
A(int a);
A(const A & a);
~A();
};
void A::set(int a)
{
m_a = a;
}
int A::get()
{
return m_a;
}
A::A()
{
cout << "无参构造!" << endl;
}
A::A(int a)
{
m_a = a;
cout << "有参构造!" << endl;
}
A::~A()
{
cout << "析构函数!" << endl;
}
A::A(const A & a)
{
m_a = a.m_a;
cout << "拷贝构造函数!" << endl;
}
int main(int argc, const char * argv[]) {
//1. 括号法
A a1; //无参构造 无参构造调用不要加(),不然编译器认为是一个函数的声明,就不会创建对象
A a2(10); //有参构造
A a3(a2); //拷贝构造
//2. 显示法
A a4;
A a5 = A(20);
A a6 = A(a5);
A(20); //为匿名对象 当前行执行结束后,系统立即回收匿名对象
//3. 隐式法
A a7 = 10; //就相当于A a7 = A(10);
A a8 = a7;
return 0;
}
关于匿名对象:例如写个 A(1);其实现在没多大用(STL中vector容器在利用swap函数以实现收缩内存的效果会用到匿名对象),执行完当前行就被释放。再就是可以利用拷贝构造函数初始化匿名对象,但不能写成A (a2); 因为编译器认为这就是 A a2; 而a2已经创建,因为拷贝构造是用已创建对象初始化新创建对象。但下面代码中可以看出确实是调用了拷贝构造函数!
class Per
{
public:
Per(int p):m_p(p)
{
cout<<"调用有参构造!"<<endl;
}
Per(const Per &p)
{
this->m_p = p.m_p;
cout<<"调用拷贝构造函数!"<<endl;
}
void show()
{
cout<<"show!"<<endl;
}
int m_p;
};
void test7()
{
Per p(10);
Per(20);
Per(p).show(); //调用拷贝构造函数!show!
}
3.3 拷贝构造函数调用时机
1>使用已创建完毕的对象去初始化一个新对象
2>值传递的方式给函数参数传递
3>以值的方式返回局部对象
3.4 构造函数调用规则
默认情况下编译器至少给一个类添加四个函数:
其一:默认构造函数(无参,函数体为空)
其二:默认析构函数(无参,函数体为空)
其三:默认拷贝构造函数,对属性进行值拷贝
其四:赋值运算符重载
构造函数调用规则如下:如果用户自己定义了有参构造函数,那么编译器不会再提供默认构造函数,但会提供默认拷贝构造函数。如果用户自己定义了拷贝构造函数,那么编译器不会再提供其他构造函数。
3.5深拷贝与浅拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
注意:同一类的不同对象析构顺序与构造顺序相反! 因为像函数参数、局部变量之类的都存放于栈区。类的不同对象也会按照构造顺序依次入栈。
class Person
{
public:
Person()
{
cout << "Person的默认构造函数调用!" << endl;
}
Person(int age, int height)
{
m_Age = age;
m_Height = new int(height); //在堆区开辟一个空间用于存放身高
cout << "Person的有参构造函数调用!" << endl;
}
Person(const Person &p)
{
cout<< "Person的拷贝构造函数调用!" << endl;
m_Age = p.m_Age;
m_Height = new int(*p.m_Height);
}
~Person()
{
if (m_Height!=NULL)
{
delete m_Height;
m_Height = NULL;
}
cout<< "Person的析构函数调用!" << endl;
}
int m_Age;
int * m_Height;
};
int main(int argc, const char * argv[])
{
Person person1(10, 100);
Person person2(person1);//如果不写拷贝构造函数,那么系统会调用默认拷贝构造函数进行浅拷贝,
//两个对象指向同一个堆空间。但在析构时,person1释放了开辟的堆区空间,
//而person2析构时又会释放一次因此报错
return 0;
}
总结:如果类的成员变量有在堆区开辟的,那么就应该自己提供拷贝构造函数,以避免出现浅拷贝带来的问题。
3.6初始化列表用于初始化属性。语法 构造函数():属性1(值1),属性2(值2)……
需要牢记 “:”后是属性(非静态成员变量或者基类),“()”里是对属性初始化的值
class Person
{
public:
Person(int a,int b, int c):m_A(a),m_B(b),m_C(c) //初始化列表
{
}
int m_A;
int m_B;
int m_C;
};
int main(int argc, const char * argv[])
{
Person person1(10,20,30);
cout << person1.m_A << "\t" << person1.m_B << "\t" << person1.m_C << endl;
return 0;
}
注意:尽量多使用初始化列表,并且初始化列表其排列顺序要与class中声明顺序一致。
如果类的成员变量是const、引用、未提供默认构造函数的类类型,那么我们必须使用构造函数的初始化列表。
如果发生了继承,父类没有默认构造也要使用初始化列表。:基类(基类继承过来的成员变量的值)
构造函数使用初始化列表与在函数体中执行赋值操作是有区别的,前者是直接初始化数据成员,后者是先初始化再赋值
class A
{
public:
A() {
cout << "class A 的无参构造函数!" << endl; }
A(int i) :m_A(i)
{
cout << "class A 的有参构造函数!" << endl;
}
A(const A & a) :m_A(a.m_A)
{
cout << "class A 的拷贝构造函数!" << endl;
}
int m_A;
};
class B
{
public:
/*B(int j) :a(j)
{
cout << "class B 的有参构造函数!" << endl;
}*/
B(int j)
{
this->a.m_A = j;
cout << "class B 的有参构造函数!" << endl;
}
//B(const B & b) :a(b.a) //这里产生对象a是通过类A的拷贝构造函数
//{
// cout << "class B 的拷贝构造函数!" << endl;
//}
B(const B & b)
{
//报错,类A不存在默认构造函数
cout << "class B 的拷贝构造函数!" << endl;
this->a = b.a; //其实这里相当于赋值,而且类A的对象早在调用拷贝构造函数时就产生了
//cout << "class B 的拷贝构造函数!" << endl;
}
A a;
};
int main(int argc, const char * argv[])
{
B b(10);
cout << b.a.m_A << endl;
B b1(b); //这里是调用拷贝构造函数,但是还是先构造了成员变量 a,也就是说先构造了a后再构造的b ,这与3.7类对象作为类成员观点一致
return 0;
}
//输出时先构造后拷贝构造
//构造使用,拷贝构造不使用初始化列表的输出
// class A 的有参构造函数!
// class B 的有参构造函数!
// 10
// class A 的无参构造函数!
// class B 的拷贝构造函数!
//构造使用,拷贝构造使用
// class A 的有参构造函数!
// class B 的有参构造函数!
// 10
// class A 的拷贝构造函数!
// class B 的拷贝构造函数!
//构造不使用,拷贝构造使用
// class A 的无参构造函数!
// class B 的有参构造函数!
// 10
// class A 的拷贝构造函数!
// class B 的拷贝构造函数!
//构造不使用,拷贝构造不使用
// class A 的无参构造函数!
// class B 的有参构造函数!
// 10
// class A 的无参构造函数!
// class B 的拷贝构造函数!
详细解释:
当类对象作为类成员时,不管通过构造函数也好拷贝构造函数也罢,都是先构造成员对象,再构造类对象观点同3.7.
但是如果使用初始化列表,那么在构造成员对象时是直接初始化数据成员,所以成员对象该有参构造就有参构造,该拷贝构造就拷贝构造;
如果不使用,那么在创建成员对象时必定先初始化化再去调用构造函数体或者拷贝构造函数体的赋值语句去赋值,所以必定要无参构造函数。
继承与成员对象的效果一致,只是初始化列表的写法有些许区别,一个是成员对象一个是基类
3.7 类对象作为类成员
一个类的对象作为另一个类的成员 例如 class A{}; class B{A a;};
class Phone
{
public:
Phone(string mphone) :m_PName(mphone)
{
cout << "调用Phone的构造函数" << endl;
}
~Phone()
{
cout<<"调用Phone的析构函数" << endl;
}
string m_PName; //手机品牌
};
class Person
{
public:
Person(string pname, string name)
:m_Phone(pname),m_Name(name) //注意:m_Phone(pname)相当于Phone m_Phone = pname
{
cout << "调用Person的构造函数" << endl;
}
~Person()
{
cout << "调用Person的析构函数" << endl;
}
Phone m_Phone;
string m_Name; //人名
};
int main(int argc, const char * argv[])
{
Person p1("张三", "华为");
cout << "姓名:" << p1.m_Name << "\t" << "手机品牌:" << p1.m_Phone.m_PName << endl;
return 0;
}
//运行结果:
//调用Phone的构造函数
//调用Person的构造函数
//姓名:华为 手机品牌:张三
//调用Person的析构函数
//调用Phone的析构函数
注意:其一,在成员是其他类对象时,初始化列表怎么写。其二构造与析构的顺序是,类对象先构造,再是自身构造,接着类对象析构,最后自身析构。
3.8 静态成员
静态成员就是在成员变量和成员函数前加关键字static,称静态成员。
静态成员分为静态成员函数与静态成员变量
静态成员变量:
1> 所有对象共享一份数据 (例如银行账号类中的成员变量基准利率,C++笔记二中有介绍)
2> 在编译阶段分配内存 (全局区)
3> 类内声明,类外初始化 (一定要类外初始化,因为类对象是放在栈区的,而静态成员变量放在全局区,而且这样做可以使得静态成员变量只被定义一次。如果又被const修饰的话,在类中声明时就定义,因为const修饰就是常量)
注意:除了上面三点外,静态成员变量可以通过类对象访问,也可以通过类直接访问。静态成员变量也是有访问权限的。
class Person
{
public:
static string m_N;
};
string Person::m_N = "张三"; //类内声明,类外初始化
int main(int argc, const char * argv[])
{
Person p1;
cout << p1.m_N << endl;
Person p2;
p2.m_N = "李四";
cout << p1.m_N << endl; //输出李四
//静态成员变量不仅可以通过类对象进行访问,还可以直接通过类访问
cout << Person::m_N << endl; //输出李四
return 0;
}
静态成员函数:
1> 所有对象共享一个函数
2> 静态成员函数只能访问静态成员变量(因为静态成员函数如果可以访问其他成员变量,那么如果要读写成员变量就不知道到底是哪个对象的成员变量)
注意:除了上面两点外,静态成员函数可以通过类对象访问,也可以通过类直接访问。静态成员函数也是有访问权限的。静态成员函数在类外定义时可以不加static关键字
4.C++对象模型与this指针
4.1成员变量与成员函数分开存储,只有非静态成员变量才属于类的对象上。
class Person
{
};
class Student
{
int m_A; //占用空间为4字节,因为int类型占4字节,不像空对象还要占1字节以区分不同对象
static int m_B; //加上静态成员变量后还是4字节,可见静态成员变量不属于类的对象上,因为类对象所占空间没变
void func1() {
} //再加上成员函数后依然不变,可见成员函数也不属于类的对象上
static void func2() {
} //最后加上静态成员函数还是不变,可见静态成员函数也不属于类的对象上
};
int main(int argc, const char * argv[])
{
Person p1;
cout << "size of p1:" << sizeof(p1) << endl; //空对象占用空间为1,因为要用地址区分不同的空对象
Student s1;
cout << "size of s1:" << sizeof(s1) << endl;
return 0;
}
注意:空对象占1字节空间
4.2由上面可知,成员函数与成员变量分开存储。而且只有非静态成员变量属于类的对象。其中非静态成员函数的添加不会影响类的对象所占用空间,所以说明非静态成员函数只会产生一分函数实例,即多个对象共用一块代码。而为了区分是哪个对象调用,需要用到this指针。
this指针特点:
this指针指向被调用的成员函数所属的对象
this指针隐含在每一个非静态成员函数里
this指针不需要定义,直接使用即可
this指针的用途:
当形参与成员变量同名时,用this指针区分
当类非静态成员函数返回对象本身时,可用 return * this; (this是指向对象的指针,所以解引用返回对象)
class Person
{
public:
Person(int m_age) //因为this是在函数体内,所以this不能用于初始化列表
{
this->m_age = m_age; //this指针的第一个用法
}
Person & AddPerAge(Person p)
{
this->m_age += p.m_age;
return *this; //this指针第二个用法
}
int m_age;
};
int main()
{
Person p1(10);
cout << p1.m_age << endl; //输出10
Person p2(20);
//链式编程思想
p2.AddPerAge(p1).AddPerAge(p1).AddPerAge(p1); //因为函数返回引用时可以充当左值,所以可以不必再创建对象去“接收”
cout << p2.m_age << endl; //输出50
return 0;
}
4.3空指针访问成员函数,再C++中空指针可以调用成员函数,但也要注意有没有用到this指针。如果用到,那么就要加判断去保证代码的健壮性。
class Person
{
public:
void ReadName()
{
cout << "类名为:Person" << endl;
}
void ReadDate()
{
//添加判断
if (this == NULL)
{
return;
}
cout << m_age << endl; //其实这里省略了this->m_age
}
int m_age;
};
void test()
{
Person *p1 = NULL;
p1->ReadName(); //运行成功!
p1->ReadDate(); //报错,因为ReadDate里用到了this指针,可以在其中加判断,提高代码的健壮性
}
int main()
{
test();
return 0;
}
4.4const修饰成员函数
常函数:
1> 成员函数后加const后我们称为这个函数为常函数,声明与定义都要加const关键字,
2> 常函数内不可修改成员属性,而且也不能返回成员变量的引用,这相当于间接修改成员变量。但可读成员变量
3> 成员属性声明时加关键字mutable后,在常函数中依然可以修改
4> 常函数也是相当于一种重载,结果是常对象调用常函数,普通对象调用普通版本函数
常对象:
1> 声明对象前加const称该对象为常对象
2> 常对象只能调用常函数
3> mutable修饰的成员变量,常对象也可以修改,但其他的就不行
class Person
{
public:
void func1() const // 在成员函数后加const,这个const修饰的是this指针,this指针变成const Person * cosnt this,指向与指向的值都不可变
{
//m_A = 100; //其实这里省略了this指针,相当于this->m_A = 100;
//其中this指针就是一个指针常量,指向不可变,但指向的内容可变,所以m_A = 100;没有报错
//this = NULL;//报错,因为this是指针常量
m_B = 200;
}
void func2() {
}
int m_A;
mutable int m_B; //成员变量加入mutable关键字后const修饰的成员函数便可对其进行修改
};
int main()
{
Person p1;
p1.func1();
cout << p1.m_B << endl;
cout << "------------------" << endl;
const Person p2;
//p2.m_A = 10;//错误,因为常对象不可修改属性,关键字mutable修饰的除外
p2.m_B = 30;//正确
p2.func1(); //正确,常对象可以调用常函数,因为常函数不可以修改属性mutable修饰的除外
//p2.func2(); //错误,因为func2有可能修改属性
return 0;
}
注意:常函数中的const修饰的是this指针,使其从指向不可变变成指向和指向的值都不可变。不管常函数还是常对象只能改变mutable修饰的成员变量。
5.友元
5.1友元目的就是为了让某些类或者函数可以访问类私有成员。关键字为:friend。
友元的三种实现:全局函数做友元、类做友元、成员函数做友元
友元要少用,友元的唯一用处就是操作符重载。
5.2全局函数做友元
class Person
{
//全局函数为类Person的友元
friend void func1(const Person &p1);
public:
Person()
{
m_A = 10;
m_B = 20;
}
int m_A;
private:
int m_B;
};
void func1(const Person &p1)
{
cout << "p1->m_A=" << p1.m_A << endl; //公有成员变量类外可以访问
cout << "p1->m_B=" << p1.m_B << endl; //添加友元后不报错
}
int main()
{
Person p;
func1(p);
return 0;
}
5.3类做友元
class Person
{
//类为类Person的友元
friend class Student;
public:
Person()
{
m_A = 10;
m_B = 20;
}
int m_A;
private:
int m_B;
};
class Student
{
public:
void func1(Person * p1)
{
cout << "p1->m_A=" << p1->m_A << endl; //公有成员变量类外可以访问
cout << "p1->m_B=" << p1->m_B << endl; //添加友元后不报错
}
};
int main()
{
Person p;
Student s;
s.func1(&p);
return 0;
}
5.4成员函数做友元
class Person;
class Fin
{
public:
Fin();
~Fin();
void func();
Person * m_p;
};
class Person
{
friend void Fin::func();
public:
Person();
string m_Name;
private:
int m_Age;
};
Person::Person()
{
m_Name = "李四";
m_Age = 20;
}
Fin::Fin()
{
m_p = new Person;
}
Fin::~Fin()
{
if (m_p != NULL)
{
delete m_p;
m_p = NULL;
}
}
void Fin::func()
{
cout << "查找姓名:" << m_p->m_Name << endl;
cout << "查找年纪:" << m_p->m_Age << endl; //未设置为友元时报错
}
void test()
{
Fin f;
f.func();
}
int main()
{
test();
return 0;
}
注意:上面代码中class Fin的定义放在class Person前然后在最前面加个class Person的声明是可以的,因为这Fin中只是要知道有个Person类型就行,所以只用声明就可以了。但是如果反过来Person定义在Fin前即使加了声明也没有用,因为设置了友元的那个语句中提到了Fin中有fun(),但提前声明只知道有Fin类,所以报错。