1、引用基本使用
1.1引用基本使用
a,b指向同一块空间,a、b的修改会影响彼此
int main() { int a = 13; //创建别名b; int& b = a; b = 100; cout << "a=" <<a<<"\n" <<"b="<<b<< endl; int c = 20; b = c;//复制操作,而不是引用 cout << "a=" << a << "\n" << "b=" << b << "\n" << "c=" <<c << endl; return 0; }
1.2注:
- 引用必须初始化
- 引用初始化后不可改变==从一而终
1.3引用作函数参数
void swap(int &A, int& B) { int tmp; tmp = B; A = B; B = tmp; } int main() { int a=10; int b=20; swap(a,b); cout<<"a="<<a<<endl; cout<<"b="<<b<<endl; return 0; }
这里使用引用作为函数参数,当调用函数给函数传参时,A为a的别名,B为b的别名,形参和实参指向同一块空间,因此形参的改变也会影响实参
1.4引用作函数返回值
1.不要返回局部变量的引用
当返回局部变量的引用时,在函数调用完成后,局部变量及其值都被销毁,引用也没有具体值
2.函数返回引用可以作左值
int& fun() { static int a = 10;//使用static修饰使其成为全局变量 return a; } int main() { int& ret = fun(); cout << "ret=" << ret << endl;//10 fun() = 100; cout << "ret=" << ret << endl;//100 return 0; }
这里的fun()函数返回值为引用,可以作为赋值操作符“=”的左值,相当于给返回的引用赋值
1.5引用的本质
引用的本质在C++内部实现时一个指针常量
int main() { int a = 10; //自动转换为 int* const ref=&a;这也就说明了引用不可更改 int& ref = a; ref = 20;//自动转换为*ref=20; }
1.6常量引用
作用:用来修饰形参,防止误操作
void print(const int& ref) { cout << "ref=" << ref << endl; } int main() { //int& ref = 10; err,引用必须引用合法的内存空间 const int& ref = 10; //加入const之后,编译器会优化代码,变为"int tmp=10;const int& ref=tmp;" //同时加入const之后便不可修改 print(ref); return 0; }
当调用函数时用引用作为形参并且不希望改变实参,可以使用const修饰
2、函数提高
2.1函数的默认参数
int func(int a, int b, int c = 10) { return a + b + c; } int func(int a, int b=10, int c ); int main() { int a = 10, b = 20, c = 30; cout <<"func(a,c)=" << func(a, c) << endl;//50 cout <<"func(a,b,c)=" << func(a, b, c) << endl;//60 }
- 如果传入数据,就用自己的数据,如果默认参数没有传入数据,那么就是用默认值
- 如果某个位置有了默认参数,则从这个位置往后,从左到右都必须有默认值
- 函数的声明和函数的实现只能其中一个有默认参数,否则会造成重定义默认参数
2.2占位参数
C++中函数的形参列表中可以有占位参数,用来占位,在调用函数时必须填补该位置
同时,占位参数还可以拥有默认参数
语法:返回值类型 函数名(数据类型){}
void func(int a, int = 10) { cout << "wanyeweiyuwenhaitang" << endl; } int main() { int a = 10; func(a, 111);//随便使用一个相应数据类型的值进行占位 func(a);//因为func的占位参数拥有默认参数,因此也可以不进行填补 return 0; }
2.3函数重载
作用:函数名可以相同,提高复用性
条件:
- 同一个作用域下(例如全局作用域下)
- 函数名称相同
- 函数参数类型不同或个数不同或者顺序不同
注意事项:
- 函数的返回值不能作为函数重载的条件
- 引用作为重载条件
void func(int& a) { cout << "func(int& a)" << endl; } void func(const int& a) { cout << "func(const int& a)" << endl; } int main() { //两个函数的参数类型不同,能够进行函数重载 int a = 10; func(a); //这里的a为变量,可读可写,而const修饰的引用为只读, //所以一般会走没有用const修饰的函数 func(10); //实参为常量时,int& a=10的引用是不能通过的 //const int& a=10则可以通过 return 0; }
- 函数重载碰到默认参数
void func(int a) { cout << "func(int& a)" << endl; } void func(int a,int b=10) { cout << "func(int a,int b=10)" << endl; } int main() { int a = 10; //函数重载碰到默认参数,出现二意性,报错 //尽量避免在函数重载使用默认参数 func(a); }
3、类和对象
C++面向对象的三大特性为:封装、继承、多态;C++认为万事万物皆为对象,对象上有其属性和行为
3.1封装
3.1.1封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
语法: class 类名{访问权限:属性/行为};
class Person { //访问权限:public 公共\protected保护\private私人 public: //属性 string m_name;//名字 int age;//年龄 //行为 int HeartRate()//最大心率 { return 220 - age; } void setName(string name)//设置名字 { m_name = name; } }; int main() { //通过Person类创建p1对象 Person p1; p1.age = 18;//给p1年龄赋值 p1.setName("only"); cout <<"p1最大心率为:" << p1.HeartRate() << endl; cout << "p1姓名为:" << p1.m_name << endl; }
3.1.2封装的访问权限
类在设计时,可以把属性和行为放在不同的控制权限下,控制访问权限有三种:
- public: 公共权限 类 内外均可访问
- protected:保护权限 类内可以访问,类外不可访问
- private: 私有权限 类内可以访问,类外不可访问
3.1.3 struct 和class的区别
在C++中struct与class的唯一区别是它们的默认权限不同
struct 的默认权限 为public
class 的默认权限 为private
3.1.4成员属性设置为私有
优点:
- 将成员属性设置为私有,能够自己通过行为来控制成员的读写权限
- 对于写权限,能够通过行为来检测数据的有效性,而不是在类外直接给属性赋值
3.2对象的初始化和清理
3.2.1构造函数和析构函数
构造函数:主要作用在于创建对象时为对象的成员进行赋值,由编译器自动调用,无须手动调用
1.语法:类名 ( ){ 函数体 };构造函数没有返回值也不写void,函数名称与类名相同
2.构造函数可以有参数,因此可以发生重载
3.程序在创建对象时会自动调用构造函数,无须手动调用,且只会调用一次
析构函数:主要用于对象在销毁前系统自动调用,执行一些清理工作
1.语法:~类名(){函数体};造函数没有返回值也不写void,函数名称与类名相同,在名称前加上符号~
2.析构函数不能有参数,因此不能重载
3.程序在对象销毁前自动调用析构,无须手动调用,且只会调用一次
3.2.2构造函数的分类和调用
分类:
1.按照有无参数: 无参构造(默认构造) 和 有参构造
2.按照类型分类: 普通构造 和 拷贝构造
class Person { public: int age; Person()//无参构造/默认构造/普通构造 {} Person(int a)//有参构造/普通构造 { age = a; } Person(const Person& p)//有参构造/拷贝构造,注意拷贝构造参数形式 { age = p.age; } };
调用:
1.括号法,常用
int main() { Person p1;//无参函数,不能写成Person p1();编译器会认为这是函数声明 Person p2(9);//有参函数 Person(int a) Person p3(p2);//构造函数调用 }
2.显示法
Person p4 = Person(10);//有参构造函数 Person p5 = Person(p4);//拷贝构造函数 //Person(10)单独写就是匿名对象,当前行结束之后,马上析构 //不要利用拷贝构造函数初始化匿名对象 //Preson (p3);编译器会认为Person(p3)==Person p3
3.隐式转换法
Person p6 = 10;//有参构造函数 Person p6=Person(10) Person p7 = p6;//拷贝构造函数 Person p7=Person(p6)
3.2.3拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象
class Person { public: int age; Person()//无参构造 {} Person(int a)//有参构造 { age = a; } Person(const Person& p)//拷贝构造,注意拷贝构造参数形式 { age = p.age; } }; int main() { Person p1(10); Person p2(p1);//用一个已经创建完毕的对象来初始化一个新对象 }
- 值传递的方式给函数参数传值
void dowork(Person p1){} void test() { Person p;//调用默认构造函数 dowork(p);//进行值传递,形参是实参的临时拷贝, //相当于Person p1=p==>> Person p1=Person(p); }
- 以值方式返回局部对象
Person dowork() { Person p1; return p1;//这里p1以值的形式返回,而不是地址的形式 } void test2() { Person p2 = dowork(); //p2在接受dowork的返回值时相当于==>>Person p2= p1; }
3.2.4构造函数调用规则
默认情况下,C++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝
构造函数调用规则如下:
如果用户定义有参构造函数,C++不在提供默认无参构造,但是会提供默认拷贝构造函数如果用户定义拷贝构造函数,C++不会再提供其他构造函数
3.2.5浅拷贝与深拷贝
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
class Person { public: int m_age; int* m_height; Person() { cout << "Person的默认构造函数调用" << endl; } Person(int age, int height) { m_age = age; m_height = new int(height); cout << "Person的有参构造函数调用" << endl; } Person(const Person& p) { 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 main() { Person p1(20, 165); cout << "p1的年龄为:" << p1.m_age << " 身高为 " << *p1.m_height << endl; Person p2(p1); cout << "p2的年龄为:" << p2.m_age << " 身高为 " << *p2.m_height << endl; //局部变量都存储在 栈区,栈区先进后出,因此在销毁p2之前会先调用析构函数 //析构函数会释放在 堆区 开辟的空间,而之后 p1的销毁也会 调用析构函数 //创建对象p2时 使用的是系统提供的拷贝构造函数,会将成员的值 逐个字节 进行拷贝 //从而使p1 p2中的height指向同一块空间,最终在p1销毁前会多次释放该空间 //解决办法是 自己写一个拷贝构造函数 return 0; }
3.2.6初始化列表
语法:构造函数():属性名1(值1),属性名2(值2)...{}
class Person { public: int m_A; int m_B; int m_C; Person() :m_A(10), m_B(20), m_C(30) {} Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) { cout << "有参构造函数的调用"<<endl; } }; int main() { Person p; //Person p(10,20,30);没有有参构造函数时,可在创建变量时直接进行初始化 cout << p.m_A <<" " << p.m_B <<" " << p.m_C << endl; Person p2(30, 20, 10);//这里则是创建对象时,调用的有参构造函数 cout << p2.m_A <<" "<< p2.m_B <<" "<< p2.m_C << endl; }
3.2.7类对象作为类成员
class Phone { public: string m_name; Phone(string name) :m_name(name) { cout << "Phone的构造函数调用" << endl; } ~Phone() { cout << "Phone的析构函数调用" << endl; } }; class Person { public: string m_name; Phone m_phone; Person(string name, string pname):m_name(name),m_phone(pname) //这里的m_phone(pname)相当于 Phone m_phone=pname //相当于构造函数调用的隐式转换法==>>Phone m_phone(pname); { cout << "Person的构造函数调用" << endl; } ~Person() { cout << "Person的析构函数调用" << endl; } }; int main() { Person p("张三", "小米"); }
通过执行结果可以看出,类对象作为类成员时:
构造时先构造 类 对象,再构造类 自身
析构时先析构 类 自身, 再析构类 对象
3.2.8静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员静态成员分为:
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数:
- 所有对象享同一个函数
- 静态成员函数只能访问静态成员变量
class Person { public: static int m_A;//类内声明 int m_B; static void func() { m_A = 666; //m_B=777;不能对非静态成员变量进行访问,无法区分是哪个对象的m_B; } }; int Person::m_A = 100;//类外初始化 int main() { Person p1; cout << "p1= " << p1.m_A << endl; Person p2; p2.m_A = 200; Person::m_A = 200; cout << "修改后 p1= " << p1.m_A << endl; //两种访问静态成员的方式: //1.通过对象访问 2.通过类名访问 //可以将静态成员看作不属于具体某个成员,而是属于这个 类 p2.func(); Person::func(); cout << "修改后 p1= " <<p1.m_A << endl; }
3.3C++对象模型和this指针
3.3.1成员变量和成员对象分开存储
在C++中,类内的成员变量和成员函数分开存储,只有非静态的成员变量才属于类的对象上
C++中空对象占一个字节
class Person { }; int main() { Person p; //C++编译器会给每个空对象也分配 一个字节 空间,是为了区分空对象 占内存的位置 cout << "空类的大小为:" << sizeof(p) << endl; }
3.3.2this指针概念
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码
那么问题是:这—块代码是如何区分那个对象调用自己的呢?
C++通过提供特殊的对象指针,this指针,解决上述问题。this指针是隐含在每一个非静态成员函数内的一种指针,不需要定义,直接使用即可this指针指向被调用的成员函数所属的对象
this指针的用途:
- 当形参和成员变是同名时,可用this指针来区分
- 在类的非静态成员函数中返回对象本身,可使用return *this 则返回值需要使用 类型&
class Person { public: int age; Person(int age) { this->age = age;//通过this指针区分形参和成员变量 } Person& func(Person p) {age += p.age; return *this;} }; int main() { Person p1(20); //此时,this指针指向 被调用的成员函数 所属的对象,也就是p1 //通过this指针来区分形参和成员变量; Person p2(30); p2.func(p1).func(p1).func(p1); //func函数的返回值如果是 Person类型,则结果为50; //这是因为Person返回类型 以值的方式返回,会重新创建一个Person p2'对象 cout << "p2的年龄为:" << p2.age << endl; }
3.3.3空指针访问成员函数
C++中空指针也可以调用成员函数,但要注意有没有使用到this指针,如果有用到this指针,需要加以判断
class Person { public: int age; void showClassName() { cout << "this class name is Person" << endl; } void showAge() { cout << "this age is " <<age<< endl; } }; int main() { Person* p = NULL; p->showAge(); p->showClassName(); }
能够看到程序出错,这是由于程序会在属性之前默认加上 this->,即:
cout << "this age is " <<this->age<< endl;
但 p指向NULL,并没有创建确切的值,因此this也为NULL;为了代码不崩溃,可加以判断:
void showAge() { if (this == NULL) return; cout << "this age is " <<this->age<< endl; }
3.3.4const修饰成员函数
常函数:
- 成员函数后加const后我们称为这个函数为常函数·
- 常函数内不可以修改成员属性
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
- 声明对象前加const称该对象为常对象
- 常对象只能调用常函数
- 常对象依然可以修改加关键字mutale的成员属性
class Person { public: int age; mutable int m_A;//即使在常函数、常对象中,也可以修改这个值 void func()const { //age = 100; m_A = 100; } //this指针的本质是 指针常量 指针的指向是不可以修改的;相当于Person* const this; //当函数后加const后,函数内的this指针 相当于const Person* const this; //让指针的值也不可以修改 void show(){} }; int main() { const Person p; //p.age = 100;不可修改 p.m_A = 100;//mutable修饰的成员属性可以修改 p.func(); //p.show;常对象只能调用常函数 //因为在非常函数中,有可能修改普通成员属性 }
3.4友元
在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的:让一个函数或者类访问另一个类中私有成员
友元的关键字为friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
3.4.1全局函数作友元
class Person { friend void godGay(Person* p); public: int height; private: int age; string password; }; void godGay(Person* p) { //通过全局函数访问私有属性需要在类中声明该函数,并在声明前加上关键字friend cout << p->age << endl; cout << p->password << endl; }
3.4.2类做友元
class Building;//提前声明Building, 方便在GoodGay中使用 class GoodGay { public: void visit(); Building* building; GoodGay(); }; class Building { friend class GoodGay;//类作为友元 public: string sittingroom; Building(); private: string bedroom; }; //类外实现函数 Building::Building() { sittingroom = "客厅"; bedroom = "卧室"; } GoodGay::GoodGay() { //创建建筑物对象 building = new Building; //在new Building 时也会调用Building的构造函数 } void GoodGay::visit() { cout << "GoodGay正在访问:" << building->sittingroom << endl; cout << "GoodGay正在访问:" << building->bedroom << endl; } int main() { GoodGay g1; g1.visit(); }
3.4.3成员函数作友元
class Building { friend void GoodGay::visit(); //GoodGay中的成员函数作为Building的友元 //注意需要在visit()前加上作用域 GoodGay:: public: string sittingroom; Building(); private: string bedroom; };
3.5运算符重载
概念:对已有的运算符就行重定义,赋予其另一种功能,以适应不同的数据类型
3.5.1加号运算符重载
class Person { public: int m_A, m_B; //Person operator+(Person& p)//使用成员函数实现 //{ // Person tmp; // tmp.m_A = this->m_A + p.m_A; // tmp.m_B = this->m_B + p.m_B; // return tmp; //} Person() { m_A = 10; m_B = 10; } }; //使用全局函数实现 Person operator+(Person& p1, Person& p2) { Person tmp; tmp.m_A = p1.m_A + p2.m_A; tmp.m_B = p1.m_B + p2.m_B; return tmp; } int main() { Person p1,p2,p3; //1.成员函数重载本质 //p3 = p1.operator+(p2); //2.全局函数重载本质 //p3=operator+(p1,p2); p3 = p2 + p1; cout << "p3.m_A=" << p3.m_A << endl; cout << "p3.m_B=" << p3.m_B<< endl; }
总结
- 对于内置的数据类型的表达式的的运算符是不可能改变的
- 不要滥用运算符重载,比如:使用operator+而在函数实现中实现的是减法
3.5.2左移运算符重载
class Person { public: int m_A; int m_B; Person(int a, int b) { m_A = a; m_B = b; } }; //只能使用全局函数实现 void operator<<(ostream& out, Person p) //cout是标准的输出流对象,全局只有一个,因此要使用引用 //本质 operator<<(out,p) 简化:out<<p { cout << " m_A=" << p.m_A << " m_B=" << p.m_B; } int main() { Person p1(13, 14); cout << p1; }
虽然上述代码能够进行打印,但是只能打印一次,不能再在其后打印,这是因为函数的返回值为void ,如果想继续追加,则须返回 标准输出流
ostream& operator<<(ostream& out, Person p) //cout是标准的输出流对象,全局只有一个,因此要使用引用 //本质 operator<<(out,p) 简化:out<<p { cout << " m_A=" << p.m_A<< " m_B=" << p.m_B; return out; } int main() { Person p1(13, 14); Person p2(5, 20); cout << p1 << p2 << endl; }
3.5.3递增运算符重载
class MyInteger { public: int myint; MyInteger() { myint = 0; } //前置++重载 MyInteger& operator++()//返回引用是为了一直对一个数据进行递增操作 { myint++; return *this; } //后置++重载 MyInteger operator++(int)//使用int作为占位参数防止重定义,且只能使用int { MyInteger tmp=*this; myint++; return tmp; } }; ostream& operator<<(ostream& out, MyInteger p) { cout << "myint="<<p.myint; return out; } int main() { MyInteger p1; cout << ++p1 << endl; cout << p1++ << endl; cout << p1 << endl; }
ostream& operator<<(ostream& out, MyInteger p) //注意这里p如果使用引用的方式,则后置++的参数传递会出现问题
3.5.4赋值运算符重载
C++编译器至少给一个类添加4个函数
- 默认构造函数(无参,函数体为空)
- 默认析构函数(无参,函数体为空)
- 默认拷贝构造函数,对属性进行值拷贝
- 赋值运算符operator=,对属性进行值拷贝
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题
class Person { public: int* m_age; Person(int age) { m_age = new int(age); } ~Person() { if (m_age != NULL) { delete m_age; m_age = NULL; } } Person& operator=(Person& p) { //编译器提供浅拷贝 //m_age = p.m_age; //先判断是否有属性在堆区,如果有,先释放 if (m_age != NULL) { delete m_age; m_age = NULL; } //深拷贝 m_age = new int(*p.m_age); //为了实现连续赋值,应返回本身; return *this; } }; int main() { Person p1(18); Person p2(28); Person p3(38); p3=p2 = p1; }
3.5.5关系运算符重载
class Person { public: string m_Name; int m_Age; Person(string name, int age) { m_Name = name; m_Age = age; } bool operator==(Person& p) { if (m_Name == p.m_Name && m_Age == p.m_Age) return true; else return false; } bool operator!=(Person& p) { if (m_Name == p.m_Name && m_Age == p.m_Age) return false; else return true; } }; int main() { Person p1("谢尔比", 20); Person p2("谢尔比", 20); if (p1 == p2) cout << "p1和p2是相等的" << endl; else cout << "p1和p2是不相等的" << endl; if (p1 != p2) cout << "p1和p2是不等的" << endl; else cout << "p1和p2是相等的" << endl; }
3.5.6函数调用运算符重载
- 函数调用运算符()也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活