C++面向对象(一) —— C++类的基础知识
概览:C++类的基础知识,包括构造函数、初始值列表、拷贝构造函数、重载赋值运算符、深拷贝与浅拷贝、static、const、new以及友元。
本文首发于我的个人博客www.colourso.top,欢迎来访。
代码全部运行于VS2019
为简化考虑,部分源码省略了
#include<iostream>
以及using namespace std
。博客后续会持续更新补充。
C++类的demo
#include <iostream>
#include <sstream>
using namespace std;
enum Sex{MALE=0,FEMALE};
class Cat
{
public:
Cat() :name("unname"), age(0),sex(Sex::MALE){}
Cat(string name, int age, Sex sex) :
name(name), age(age), sex(sex) {}
//拷贝构造
Cat(const Cat& other);
//拷贝赋值运算符重载
Cat& operator=(const Cat& other);
~Cat() {}
string getName();
void setName(string str);
int getAge();
void setAge(int num);
Sex getSex();
static string getPetOwner();
static void setPrtOwner(string owner);
string toString();
private:
string name;
int age;
Sex sex;
static string petOwner;
};
string Cat::petOwner = "Colourso";
Cat::Cat(const Cat& other)
{
this->name = other.name;
this->age = other.age;
this->sex = other.sex;
}
Cat& Cat::operator=(const Cat& other)
{
if(this == &other)//自赋值
return *this;
this->name = other.name;
this->age = other.age;
this->sex = other.sex;
return *this;
}
string Cat::getName()
{
return this->name;
}
void Cat::setName(string str)
{
this->name = str;
}
int Cat::getAge()
{
return this->age;
}
void Cat::setAge(int num)
{
this->age = num;
}
Sex Cat::getSex()
{
return this->sex;
}
string Cat::getPetOwner()
{
return petOwner;
}
void Cat::setPrtOwner(string owner)
{
petOwner = owner;
}
string Cat::toString()
{
stringstream ss;
ss << "Name: ";
ss << this->name;
ss << ",age: ";
ss << this->age;
ss << ",sex: ";
ss << (this->sex ? "male" : "female");
ss << ". Owner: ";
ss << petOwner;
return ss.str();
}
int main()
{
Cat* a = new Cat("Kelly", 4, Sex::FEMALE);
cout << a->toString()<< endl;
Cat b("Bob",3,Sex::MALE);
cout << b.getName()<<" love his owner: "<<Cat::getPetOwner()<< endl;
if (b.getAge() < a->getAge())
b.setAge(a->getAge() + 2);
cout << b.toString() << endl;
delete a;
return 0;
}
class与struct
class Dog
{
public:
Dog(string name)
{
this->name = name;
}
void walk()
{
cout <<"Dog "<< this->name <<" walk." << endl;
}
private:
string name;
};
struct Cat
{
string name;
Cat(string name)
{
this->name = name;
}
void walk()
{
cout << "Cat " << this->name << " walk." << endl;
}
};
int main()
{
Dog wang("meow");
wang.walk();//Dog meow walk.
Cat hua("wang");
hua.walk();//Cat wang walk.
return 0;
}
如上所示,在C++中,我们可以使用class
关键字或者struct
关键字进行类的定义。
两者唯一的区别在于struct
与class
的默认访问权限不同。
- struct默认访问权限为public。
- class默认访问权限为private。
在类的外部实现类的成员函数——类的分文件编写
Student.h
#ifndef STUDENT_H__
#define STUDENT_H__
#include <iostream>
using namespace std;
class Student
{
public:
Student();
Student(string name, int age, bool sex);
void show();
private:
string name;
int age;
bool sex;
};
#endif
Student.cpp
#include "Student.h"
Student::Student()
{
}
Student::Student(string name, int age, bool sex)
{
this->name = name;
this->age = age;
this->sex = sex;
}
void Student::show()
{
cout << this->name << ": age:" << this->age << ", sex:" << (this->sex ? "male" : "female");
}
访问权限
class Student
{
public:
string name;
protected:
bool sex;
public:
Student(){}
Student(string name, int age, bool sex)
{
this->name = name;
this->age = age;
this->sex = sex;
}
void ShowMessage()
{
cout << this->name << ": age:" << this->age << ",sex:" << (this->sex ? "男":"女" ) << endl;
}
private:
int age;
};
int main()
{
Student s("小明", 12, true);
cout << s.name << endl;//小明
//cout << s.age << endl; //错误:不可访问
s.ShowMessage();//小明: age:12,sex:男
return 0;
}
(如上所示的糟糕编码,只是为了展示类内的访问权限的界限是从一个界限开始到下一个界限截至的,例如public到private之间的内容权限都是public,同时一个访问说明符可以出现多次。)
- public:表示公有,类内类外整个程序内都可以被访问。
- private:表示私有,只有类的成员函数才可以访问,C++ class的默认访问权限是private。
- protected:表示保护,平时与private无区别,仅在继承时有区别。
类的构造函数
class Student
{
public:
// 默认构造函数
Student() {}
//重载带参构造函数
Student(string name)
{
this->name = name;
}
string dis()
{
return this->name;
}
private:
string name;
};
class SuperStudent
{
public:
//默认构造函数
SuperStudent() {}
//带参构造函数
SuperStudent(int age)
{
this->age = age;
}
//构造函数初始值列表
SuperStudent(Student stu,int age) :stu(stu),age(age) {}
void show()
{
cout << this->stu.dis() << this->age << endl;
}
private:
Student stu;
int age;
};
int main()
{
Student s("小明");
SuperStudent s1(s, 12);
s1.show();//小明12
SuperStudent s2(12);
s2.show();//12
return 0;
}
构造函数是用于初始化类对象的非static数据成员,无论何时,只要类的对象被创建,那么就会执行构造函数。
- 在形式上,构造函数无返回类型,函数名与类名相同,一般声明为public,且构造函数有(可能为空)参数列表与(可能为空)函数体。
- 一个类可包含多个构造函数,和其他重载函数类似,不同构造函数必须在参数数量或参数类型上有所不同。
- 一个类有自己的默认构造函数,默认构造函数无需任何实参。如果存在类内初始值,则默认构造参数使用它来初始化成员,否则将默认初始化成员(赋予对应类型的默认值,eg,string类型的默认值为空串)。
- 同时如果我们没有显示定义构造函数,编译器就会隐式的定义一个。(只有当没有声明任何构造函数时,编译器才会自动生成默认构造函数)
- 一个类最好定义它的默认构造函数。(如果我们显示定义了一些其他构造函数,那么建议要加上默认构造函数)P236.
- 构造函数不能够被声明成const。7.1.2
构造函数初始值列表
class Student
{
public:
//默认构造函数
Student() {}
//构造函数初始值列表
Student(char str[], bool sex, int age) :m_sex(sex),m_age(age)
{
// m_sex = sex; //错误:表达式必须是可修改的左值
strcpy_s(m_name, str);
}
void show()
{
cout << m_name << ": sex: " << (m_sex ? "male" : "female") << ", age:" << m_age << endl;
}
private:
char m_name[20];
const bool m_sex = true;
int m_age;
};
int main()
{
char str[] = "Bob";
Student s(str, true, 12);
s.show();//Bob: sex: male, age:12
return 0;
}
- 使用构造函数初始值列表时,数组应当在构造函数体内进行赋值,而不能在初始化列表中初始化。
- 常量成员或者引用类型以及某种未提供默认构造函数的类类型成员必须要在初始化列表中进行,在函数体内就会报错!
- 建议使用构造函数初始值的习惯,因为一部分数据成员只能使用这种方式初始化,同时相对于赋值的方式,构造函数初始值列表的效率更高!
拷贝构造函数
class Foo
{
public:
//默认构造函数
Foo() {};
Foo(string str, int num) :str(str), num(num)
{ }
//拷贝构造函数
Foo(const Foo& foo) :str(foo.str), num(foo.num)
{
times++;
}
void show()
{
cout << this->str << "-" << this->num << "-" << times << endl;
}
static int times;
private:
string str;
int num;
};
int Foo::times = 0;
int main()
{
Foo f1("hello", 10); //直接初始化
f1.show(); //hello-10-0
Foo f2 = f1; //拷贝初始化
f2.show(); //hello-10-1
Foo f3(f1); //拷贝初始化,和上面那种类似
f3.show(); //hello-10-2
return 0;
}
- 一般情况下,拷贝构造函数的作用是,将给定对象中的每个非static成员拷贝到正在创建的对象之中。
- 拷贝构造函数形式就是
Foo(const Foo& foo)
,即函数参数为一个const引用类型。 - 如果我们没有定义一个拷贝构造函数,那么编译器就会自动为我们定义一个。
- 每个成员的类型决定了它如何被拷贝,类的类型成员将会使用它的拷贝构造函数来拷贝,而内置类型的成员将会直接拷贝。至于数组,将会被逐个元素地拷贝。
- 但是如何成员中有指针!必须小心的对待!!!
拷贝初始化发生情况
- 拷贝初始化一般发生于用
=
定义变量时,即Foo f2 = f1
这种形式,或者是使用对象初始化的形式,即Foo f3(f1)
。 - 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 使用花括号列表初始化一个数组中的元素或者聚合类的成员。
从上述可以看出拷贝构造函数用于初始化非引用类型参数,这一特性导致它的参数必须是引用类型,否则将会无限循环了。
拷贝赋值运算符
class Foo
{
public:
//默认构造函数
Foo() {};
Foo(string str, int num):str(str),num(num) {}
//拷贝赋值,重载等号
Foo& operator=(const Foo& foo)
{
if(this == &other)//自赋值
return *this;
this->str = foo.str;
this->num = foo.num;
return *this;
}
void show()
{
times++;
cout << this->str << "-" << this->num<<"-"<<times<<endl;
}
static int times;
private:
string str;
int num;
};
int Foo::times = 0;
int main()
{
Foo f1("hello",10); //直接初始化
f1.show(); //hello-10-1
Foo f2; //默认初始化
f2 = f1; //使用拷贝赋值运算符
f2.show(); //hello-10-2
return 0;
}
- 一般情况下,拷贝赋值运算符是将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员。
- 拷贝赋值运算符实际上就是重载赋值运算符。
- 重载赋值运算符必须定义成为成员函数。(参看链接:)
- 若运算符是成员函数,则运算对象就绑定到了隐式的this参数上,而对于一个二元运算符,右侧运算对象就作为显示的参数传递。
- 为了与内置类型保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
析构函数
class Foo
{
public:
//默认构造函数
Foo() {};
Foo(string str, int num):str(str),num(num) {}
~Foo()
{
cout << "函数析构了" << endl;
}
private:
string str;
int num;
};
int main()
{
Foo f1("hello",10); //直接初始化
return 0;
}
//执行结果:函数析构了
- 作用:析构函数释放掉对象使用的资源,并且销毁对象的非static数据成员。
- 析构函数形式上是
~类名()
,波浪线与类名组成的,无返回值,不接受参数,故析构函数不能够重载,——一个类的析构函数唯一。 - 如果不显示写出析构函数,那么编译器会自动添加一个默认的构造函数。
- 析构时,成员的销毁完全依赖于成员的类型,对应类型执行对应的析构函数,而内置类型则没有析构函数,故销毁内置类型成员不需要做什么。
- 而隐式的销毁一个内置指针类型的成员时,不会delete它所指向的对象。
什么时候调用析构函数
- 变量在离开其作用域时被销毁
- 对象被销毁时,其成员被销毁的时候
- 容器(标准库容器或者数组)被销毁时,其元素被销毁的时候
- 对于动态分配的对象,执行它的指针被delete运算符执行时
- 对于临时对象,创建它的完整表达式结束时被销毁。
深拷贝与浅拷贝
class Student
{
public:
Student();
~Student();
private:
int age;
char* name;
};
Student::Student()
{
this->name = new char(20);
this->age = 0;
cout << "Student()" << endl;
}
Student::~Student()
{
cout << "~Student()" <<(int)name<< endl;
delete name;
cout << "Ok" << endl;
name = nullptr;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2 = s1;
}//花括号结束,对象析构
cout << "hello" << endl;
return 0;
}
执行结果为引发异常,触发断点。
Student()
~Student()17464200
OK
~Student()17464200
当对一个对象进行拷贝的时候,编译器会自动调用拷贝构造函数
。而默认的拷贝构造函数就是简单的将原对象的成员一一赋值给新对象。这就是所谓的浅拷贝。
对于指针这种类型来说,原拷贝构造函数的动作就像char * a = new char(20)
,然后char* b = a
,也就是说a与b两个指针指向了同一块内存区域。当对象被析构的时候,同一块内存区域就被delete
了两次,因而触发异常。
而这种情况,一般是默认拷贝构造函数无法避免的。
因此对于类的成员含有指针的情况,一定要显示的写出拷贝构造函数,并且特殊针对指针类型进行处理,这就是深拷贝。
class Student
{
public:
Student();
~Student();
Student(const Student& other);
private:
int age;
char* name;
};
Student::Student()
{
this->name = new char(20);
this->age = 0;
cout << "Student()" << endl;
}
Student::Student(const Student& other)
{
this->age = other.age;
this->name = new char(20);
memcpy(this->name,other.name,strlen(other.name));
}
Student::~Student()
{
cout << "~Student()" << (int)name << endl;
delete name;
cout << "Ok" << endl;
name = nullptr;
}
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2 = s1;
}
cout << "hello" << endl;
return 0;
}
执行结果:
Student()
~Student()17149496
Ok
~Student()17149832
Ok
hello
参考链接:C++面试题之浅拷贝和深拷贝的区别
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
再说几句:
当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:
1.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
2.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。
3.浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。
知乎大佬Milo Yip的回答:如何理解 C++ 中的深拷贝和浅拷贝? - Milo Yip的回答
事实上,所谓的浅拷贝和深拷贝各自代表不同的意义,各有所需。关键是要区分值语意(value semantics)和引用语意(reference semantics)。
对于值语意的对象,在x= y完成复制之后,y的状态改变不能影响到x,这特性称为独立性(independence)。使用深拷贝的方式可以完全复制一个独立于原来的对象。C++提供的(模板)大部分都是值语意的,如stad::basic string、 std:.vector 等。
有些对象会引用相同的对象,举个游戏中的例子。通常多个模型可以引用同一个材质,复制模型时并不会深度复制新一份材质。 如果需要改变个别模型的材质里的参数,才会手动把该模型的材质复制,独立于其他模型的材质。
static修饰的类成员
class Student
{
public:
Student() :age(0) {}
Student(int age) :age(age) {}
~Student() {}
static int conut;
static int getConut()
{
return conut;
}
private:
int age;
static int nums;
};
int Student::conut = 12;
int Student::nums = 10;
int main()
{
Student s;
cout << s.conut << endl;
cout << Student::getConut() << endl;
//cout << s.nums << endl;//错误,无法访问private成员
return 0;
}
- static修饰类中成员,表示类的共享数据。
- static不属于某一个对象。而是此类所有的成员都公有的。
- 创建一个对象所开辟的空间中不包括静态成员的空间。静态成员的空间是在所有对象之外单独去开辟的。静态成员开辟的空间存放于静态区。
- 静态数据成员是在开始运行时被分配空间,到程序结束时才释放空间。
- 静态成员可以初始化,但是只能够在类外初始化。也不能够用参数列表对其初始化。
- 静态数据成员既可以通过对象名(a.C)或者类名(A::C)来引用。
- static修饰的成员仍然遵循public,private,protected访问准则。
- 静态成员函数的意义不在于信息共享,而在于管理静态数据成员,完成封装。
- 静态成员函数只能访问静态数据成员,因为静态成员函数属于类所有而非对象所有,没有this指针。
- 静态数据成员不占据类的大小。
this指针
this指针是隐含的变量,表示当前对象。实际就是指向了对象的地址。
在类的成员函数,非static函数内,this隐含在其中。
- 在平常情况下,this指针指向的普通成员变量的值可以改变,说明this指针不是
const Class *
,但是this指针不能做++
操作,说明this指针是一个常指针,即Class * const
。 - 而如果在成员函数的末尾加上const,则表示this指针变成了
const Class * const
这样。 - 静态成员函数没有this指针
成员函数返回对象本身
class Example
{
public:
Example() :a(10) {}
~Example() {}
Example& copy(const Example& other)
{
this->a = other.a;
return *this;
}//注意函数返回值为引用。
void showInfo()
{
cout << a << endl;
}
private:
int a;
};
const修饰的类成员
class Example
{
public:
Example() :i_nom_n(11) {}
~Example() {}
void showInfo()
{
this->i_nom_n += 100;
cout << i_con_n << " " << i_nom_n << " " << i_sta_n << " " << i_con_sta_n << endl;
}
void showInfo2() const
{
//this->i_nom_n++;//错误,表达式必须是可修改的左值
cout << i_con_n << " " << i_nom_n << " " << i_sta_n << " " << i_con_sta_n << endl;
}
private:
const int i_con_n = 10;//可在此处赋值或者初始值列表进行初始化
int i_nom_n;//可在此处赋值,为默认值
static int i_sta_n;//必须在类外初始化
const static int i_con_sta_n;//可在此处赋值或者类外初始化
};
int Example::i_sta_n = 12;
const int Example::i_con_sta_n = 13;
int main()
{
Example ex;
ex.showInfo2();
ex.showInfo();
return 0;
}
- const类常量只能在声明时就初始化或者使用构造函数初始值列表进行初始化。
- 而static const修饰的成员变量则可以在声明时初始化,或者在类外进行初始化。
- 类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
- 本质上,const指针修饰的是被隐藏的this指针所指向的内存空间,修饰的是this指针。
- 在成员函数的末尾加上const,则表示this指针变成了
const Class * const
这样。
友元
class A
{
public:
A() :a(0), b(0) {}
A(int a, int b) :a(a), b(b) {}
~A() {}
private:
int a;
int b;
friend class B;
friend void friendfun(A an, B bn);
};
class B
{
public:
B() :c(0) {}
B(int c) :c(c) {}
void info(A an)
{
cout << an.a << " " << an.b <<" "<<this->c<< endl;
}
private:
int c;
friend void friendfun(A an, B bn);
};
void friendfun(A an,B bn)
{
cout << an.a << " " << an.b << " " << bn.c << endl;
}
int main()
{
A an(1, 2);
B bn(3);
bn.info(an);
friendfun(an,bn);
return 0;
}
- 当类实现了数据的封装和隐藏的时候,又是需要一些函数频繁访问类的数据成员,但它又不是此类的一部分,此时可以将这些函数定义为友元。
- 友元将无视权限,同时友元也不受所在private、public或者protected区域的影响,一般将友元放在类的末尾部分。
- 友元的作用是提高程序运行效率,缺点是破坏了类的封装和隐藏性。
- 友元不能被继承,友元不具有传递性,友元关系是单向的。
创建对象实例的方式(new与不new)
class A
{
public:
A() :a(0), b(0) {}
A(int a, int b) :a(a), b(b) {}
~A(){}
void showInfo()
{
cout << a << " " << b << endl;
}
private:
int a;
int b;
};
int main()
{
A a1(5, 5);
a1.showInfo(); //5 5
cout << sizeof(a1) << endl; //8
A* a2 = new A(5, 5);
a2->showInfo(); //5 5
cout << sizeof(a2) << endl; //4
cout << sizeof(*a2) << endl;//8
delete a2;
return 0;
}
C++创建对象实例有两种方式:
ClassName obj(param);
ClassName * obj = new ClassName(param);
这两种方式还是有较大的区别。
ClassName obj(param);
这种方式创建的对象,内存是分配到栈中的。由编译器默认调用构造与析构函数。ClassName * obj = new ClassName(param);
,这种方式创建的对象位于堆上,new返回的是一个对象指针,这个指针指向一个对象的地址。- new会自动触发构造函数,使用new创建的对象,最后要手动使用delete,delete会自动触发析构函数。
- new创建的对象需要使用
->
来调用对应的成员。
参考链接:C++创建对象的两种方法
其他
成员初始化的顺序
- 成员初始化的顺序与他们在类定义中的出现顺序一致
- 构造函数初始值列表的顺序不影响被初始化的顺序。
- 建议是尽可能避免使用类的某些成员去初始化其他成员。
未完,等待后续慢慢补充。
本文首发于我的个人博客www.colourso.top