C++学习笔记
文章目录
注:本部分内容主要来自中国大学MOOC北京邮电大学崔毅东的《C++程序设计》课程。
第4单元 对象和类(基础)
单元导读
本单元内容,主要是声明类、用类定义对象,以及初步使用对象编写程序的方法。
本单元中最容易让人迷惑的就是对象的初始化、对象内部的数据成员的初始化问题。
本单元中难以理解的概念是this
指针。这个特殊的指针,很难简单地用文字描述清楚它的含义。想要理解this指针,一定要看相关的代码,把this放到代码中理解。
本单元介绍了两个C++的类,分别是string
和array
。
- 在C++中,我们不应该再使用C风格的字符串,也不应再使用字符数组存储字符串,而应改为使用 string 对象。
- C++11 在标准库中添加了 array 这个类。今后我们在写C++代码时,除非有明确的理由,否则我们都应该使用 array 类,而避免使用 C风格的原生数组。
4.1 用类创建对象
4.1.1 对象和类
类 是一种 数据类型 (type)。数据类型分为自定义数据类型(使用 类class 定义)和 原生数据类型(如int, char等)。对象和变量的定义形式非常类似,原生数据类型定义变量(如int x;
), 自定义数据类型 声明对象(如Class C{}; C ca;
)。 对象(object)是类(class)的实例 。
- 一个比喻:类就好比是苹果派的配料表,对象就是苹果派。
从概念上来说,一个对象表示现实世界中一个独一无二的实体,如一个学生,一张桌子,一个圆圈等。对象的三个主要要素:唯一的标识(名字)、状态(数据)、行为(函数)。
对象的状态由数据域(也称为“属性”)及其当前值构成。
对象的行为由一组函数定义。
类包含:数据域(data fields)、行为(behaviors)。与上述的对象可以对应上。
类中有两种特殊的函数:构造函数(constructors, ctor)、析构函数(destructors, dtor)。
- 一个重要的编程思想是 面向对象编程 (Object-Oriented Programming, OOP)。
- 面向对象的特征是(Features of OOP)
- Abstraction(抽象)
- Polymorphism(多态)
- Inheritance(继承)
- Encapsulation(封装)
中文含义是:一块甜饼。 这样记忆就方便多了。
4.1.2 创建对象并访问
下面先展示一个创建对象并访问的代码示例:
/*程序功能:创建类,并无参/有参创建对象*/
#include<iostream>
#include<iomanip>//定义输出精度
#include<math.h>//使用pi
#define PI acos(-1)
using std::cin;
using std::cout;
using std::endl;
//创建Circle类
class Circle {
public://没有这一行以下全是私有的
//半径
double radius;
// 无参构造函数(默认)
Circle() {
radius = 1;
}
// 有参构造函数,与上述重载函数
Circle(double newRadius) {
radius = newRadius;
}
// 调用时称为实例函数
double getArea() {
return (radius * radius * PI);
}
};//一定注意类的结束有分号
int main() {
Circle circle1; //无参创建对象
Circle circle2{5.0};//有参创建对象
cout << std::setiosflags(std::ios::fixed) <<
std::setprecision(2);//定义小数输出精度为2位
cout << "半径:" << circle1.radius <<
" 面积:" << circle1.getArea() << endl;
cout << "半径:" << circle2.radius <<
" 面积:" << circle2.getArea() << endl;
circle2.radius = 100.0;//实例变量
cout << "半径:" << circle2.radius <<
" 面积:" << circle2.getArea() << endl;
cin.get();//防止控制台一闪而过
return 0;
}
- 结果
半径:1.00 面积:3.14
半径:5.00 面积:78.54
半径:100.00 面积:31415.93
由上述示例可以看出, 构造函数 的特点有:
- Automatic invocation(自动调用)
- Has the same name as the defining class (与类同名)
- NO return value (including “void”); (无返回值)
- Can be overloaded (可重载)
- May have no arguments (可不带参数)
类可不声明构造函数。编译器会提供一个带有空函数体的 无参构造函数 ,只有当未明确声明构造函数时,编译器才会提供这个构造函数,并称之为 “默认构造函数” 。
下面看几个 无参/有参创建对象 的代码示例:
Circle circle1; // 正确,不推荐。
Circle circle2(); // 错误!C++编译器认为这是一个函数声明。
Circle circle3{}; // 正确,推荐。空初始化列表初始化circle3对象(调用Circle默认构造函数)
Circle circle4{5.5}; // 正确,C++11列表初始化自带窄化检查(narrowing check)
“ . ”点运算符,也称为 对象成员访问运算符 ,用于访问对象中的数据和函数。实际创建的对象中的变量称为 实例变量 ,并不是类的数据成员。
4.2 对象拷贝、分离声明与实现
4.2.1 对象拷贝和匿名对象
对象拷贝 (Object Copy)就是使用赋值运算符 “ = ” 可以将右边的对象拷贝到左边的对象中。默认情况下,对象中的每个数据域都被拷贝到另一对象的对应部分,而不是将对象中的函数成员进行一对一拷贝。
有时候需要创建一个只用一次的对象,这种不命名的对象叫做 匿名对象 (Anonymous Object)。下面有几个例子:
auto c1 = Circle{2.2};//用匿名对象做拷贝列表初始化
Circle c2{}; //调用默认构造函数
c2 = Circle{3.3}; //用匿名对象赋值
cout << Circle{4.2}.getArea() << endl;//创建匿名对象,直接调用成员函数
注意:C++中的class
用来替代C语言中的struct
。 两者唯一的区别是struct
成员默认公有,class
成员默认私有(变成公有需要加public
)。
局部类 和 嵌套类 一般很少使用。下面给出示例:
//局部类是在一个函数中声明的类
void f(){//C及对象只在f()中可用
class C{
void g(){//成员函数必须在C中实现
/*访问f()的成员受限*/
}
};
C c1,c2;//函数f()的局部类
}
//嵌套类是在另一个类中声明的类--Java常用这种逻辑
class E{//N及其对象可访问E的成员
class N{
void g(){};
}n1;//注意这里这个东西
};
E e1{};
e1.n1.g{};//这里就是嵌套调用
注:名字空间在证明时也可以嵌套。
这篇文章C++中的嵌套类和局部类给出了嵌套类的具体测试代码。
下面给出 对象拷贝 代码示例:
#include<iostream>
class square {
private:
double side_de = 1.0;
public:
square() = default;//C++11强制编译器生成默认构造函数
square(double side) {
this->side_de = side;//说把side赋给私有的side_de
}
double getarea() {
return (side_de * side_de);
}
};
int main() {
square s1, s2{ 4.0 };
std::cout << "s1面积:" << s1.getarea() << std::endl;
std::cout << "s2面积:" << s2.getarea() << std::endl;
s1 = s2;
std::cout << "s1面积:" << s1.getarea() << std::endl;
std::cout << "s2面积:" << s2.getarea() << std::endl;
std::cin.get();
return 0;
}
下面给出 匿名对象 代码示例:
#include<iostream>
using std::cin;
using std::cout;
using std::endl;
//创建一个银行账户类,缺点在于本代码均为一次性使用
class account {
double balance;
public:
//默认构造函数
account() {
balance = 0.0;
}
//重载函数
account(double balance_) {
balance = balance_;//效果同“this->balance=balance”
}
//存钱
double deposit(double amount) {
balance += amount;
cout << "存钱成功!余额:" << balance << endl;
return (balance);
}
//取钱
double withdraw(double amount) {
if (amount > balance) {
cout << "余额不足!余额:" << balance << endl;
return (balance);
}
else {
balance -= amount;
cout << "取钱成功!余额:" << balance << endl;
return (balance);
}
}
};
int main() {
account a1;//调用默认构造函数
account a2 = account{ 100.0 };//用匿名对象做拷贝列表初始化
a1.deposit(10.0);//注意这里不能用花括号
a1.withdraw(6.0);
a2.withdraw(58.0);
//创建匿名对象,直接调用成员函数
account{ 1000.0 }.withdraw(1010.0);//由于匿名对象无法找回返回值,所以都是一次性调用
account{ 1000.0 }.withdraw(990.0);
cin.get();
return 0;
}
4.2.2 声明与实现分离、避免头文件被多次包含
在C语言中, 声明与实现分离(Separating Declaration from Implementation)的实现依靠将头文件、子函数源文件、主函数源文件调用三者分开来实现。而在C++中,使用类进行声明和实现的分离。
-
类的实现
- 调用格式 :FunctionType ClassName::FunctionName (Arguments) { //… }
- 其中,“ :: ”称为二元作用域解析运算符,简称 域分隔符 。
-
类的声明
- 当函数在类声明中实现,它自动成为内联函数。
C++使用预处理指令(Preprocessing Directives)避免头文件被多次包含(保证头文件只被包含一次)。
//方法一(最常用):
#ifndef MY_HEADER_FILE_H
#define MY_HEADER_FILE_H
//头文件内容
#endif
//方法二:
#pragma once //C++03,C90
//方法三:
_Pragma(“once”) //C++11,C99
方法一原理是第一次执行宏的时候满足条件“if non define MACRO”(注意不要遗漏指令里的n),第二次执行就不满足了,所以只会被定义一次。所有的C++编译器都能使用。注意文件名中的点“ . ”要变成下划线“ _ ”。
方法二中#pragma
预处理指令是早就引入的标准,但是once
不是标准的,但是目前能见到的绝大多数编译器都支持。含有这个指令的文件只被编译处理一次。
方法三中_Pragma
实际是一个运算符(operator),可以在宏里面使用,原理和方法二相同。在Visual Studio中曾自己定义过__pragma
(两个下划线)取代_Pragma
(曾经不支持),但是2022.2.8使用VS2022发现也支持_Pragma
写法。
下面给出示例代码及运行结果:
- 源文件
/*将声明与实现分离*/
#include<iostream>
using std::cin;
using std::cout;
using std::endl;
//声明一个类
class circle {
double radius;//私有
public:
circle();//默认构造函数
circle(double r);//有参构造函数
double getarea();//实例函数
};
//默认构造函数的实现
circle::circle() {
radius = 1.0;//这里需要初始值是因为radius默认私有
}
//有参构造函数的实现
circle::circle(double r) {
radius = r;
}
//实例函数的实现
double circle::getarea() {
return(3.14 * radius * radius);
}
int main() {
circle c1;
circle c2{ 2.0 };
cout << c1.getarea() << endl;
cout << c2.getarea() << endl;
cin.get();
return 0;
}
- 结果
3.14
12.56
更进一步,可以将上述代码分离成几个不同的文件。如下所示:
- 头文件circle.h
//_Pragma("once")
//__pragma(once)
//#pragma once
#ifndef CIRCLE_H_
#define CIRCLE_H_
//声明一个类
class Circle {
double radius;//私有
public:
Circle();//默认构造函数
Circle(double r);//有参构造函数
double getarea();//实例函数
};
#endif
- 源文件circle.cpp
#include"Circle.h"
//默认构造函数的实现
Circle::Circle() {
radius = 1.0;//这里需要初始值是因为radius默认私有
}
//有参构造函数的实现
Circle::Circle(double r) {
radius = r;
}
//实例函数的实现
double Circle::getarea() {
return(3.14 * radius * radius);
}
- 源文件main.cpp
/*将声明与实现分离*/
#include<iostream>
#include"Circle.h"
#include"Circle.h"//用来测试代码是否可以防止头文件被多次包含
//注:这里不能添加“Circle.cpp”,否则相当于重复类的实现
using std::cin;
using std::cout;
using std::endl;
int main() {
Circle c1;
Circle c2{ 2.0 };
cout << c1.getarea() << endl;
cout << c2.getarea() << endl;
cin.get();
return 0;
}
注意: 无参/有参构造函数的名字应该与类的名字相同。
假如在源文件main.cpp中再次定义函数circle.cpp,将会构成语法错误 重定义函数 。注意这不是重载(overload),因为main.cpp和Circle.cpp的中调用的函数参数相同;也不是重写(override),因为override只存在于继承链上。
假如在源文件中创建常函数(如Double Circle::SetRadius() const {}
),那么这个常函数不能修改数据成员的值,要保持函数的状态不发生变化。
4.3 对象指针、对象数组、函数参数
4.3.1 对象指针与动态对象
创建一个 对象指针 (Object Pointer),从而使用指针访问对象的成员,包括数据成员和函数成员。这个访问的过程需要使用箭头运算符“ -> ”。当然,对象指针也可以(移情别恋)指向新的对象名。下面给出(忠心耿耿的)示例:
Circle circle1; //创建对象,默认构造函数
Circle* pCircle = &circle1; //创建对象指针
//使用解引用的方式访问对象成员
cout << "The radius is " << (*pCircle).radius << endl;//数据成员
cout << "The area is " << (*pCircle).getArea() << endl;//函数成员
(*pCircle).radius = 5.5; //有参构造函数
//pCircle->radius = 5.5; //与上一行等价
//使用箭头运算符访问对象成员
cout << "The radius is " << pCircle->radius << endl;
cout << "The area is " << pCircle->getArea() << endl;
注:上述使用解引用的方式访问对象成员时,一定要用括号括起来,因为点运算符的优先级比星号要高。
在函数中声明的对象都在栈上创建,函数返回,则对象被销毁。为保留对象,可以用new运算符在堆上创建它,称之为 动态对象 (Dynamic Object)。假如是对象数组,记得加中括号“ [ ] ”。下面是一些示例:
Circle *pCircle = new Circle{}; //用无参构造函数创建对象
Circle *pCircle = new Circle{5.9}; //用有参构造函数创建对象
//程序结束时,动态对象会被销毁,或者
delete pCircle; //用delete显式销毁
4.3.2 对象数组
那就直接看几个声明方式的示例好了:
//声明方式1
Circle ca1[10];//声明一个带有10个Circle对象的数组
//声明方式2:用匿名对象构成的列表初始化数组
Circle ca2[3] = { Circle{3}, Circle{ }, Circle{5} };
// 注意:不可以写成:auto ca2[3]= 因为声明数组时不能用auto
//声明方式3:用C++11列表初始化,列表成员为隐式构造的匿名对象
Circle ca3[3] { 3.1, {}, 5 }; //直接列表初始化
Circle ca4[3] = { 3.1, {}, 5 }; //拷贝列表初始化
//声明方式4:用new在堆区生成对象数组
auto* p1 = new Circle[3];//对象指针可以用auto
auto p2 = new Circle[3]{ 3.1, {}, 5 };
delete [] p1;
delete [] p2;
p1 = p2 = nullptr;//注:接着再删除空指针不会带来异常
对于使用堆创建的对象数组,最后假如忘记释放内存(delete
),而是直接使其等于空指针,就会导致这段内存一直被占用且不能使用指针访问对象数组。
假如重复写delete [] p1;
编译可以通过,但在运行时会报错。
4.3.3 对象作为函数参数、对象作为函数返回值
对象作为函数参数,可以 按值传递、按引用传递、按指针传递 。下面是代码示例:
/*示例一:对象作为函数参数*/
// 按值传递
void print( Circle c ) {
/* … */
}
int main() {
Circle myCircle(5.0);
print( myCircle ); //把myCircle的值拷贝给c
/* … */
}
/*示例二:对象引用作为函数参数*/
// 按引用传递
void print( Circle& c ) {
/* … */
}
int main() {
Circle myCircle(5.0);
print( myCircle ); //c是myCircle的别名,本质也是传值
/* … */
}
/*示例三:对象指针作为函数参数*/
// 按指针传递
void print( Circle* c ) {
/* … */
}
int main() {
Circle myCircle(5.0);
print( &myCircle ); //把myCircle的地址存入c中,本质也是传值
/* … */
}
注意:由于自定义函数print()
只是输出Circle对象的值,因此如果print()
作为成员函数(指在一个类内)应该用const
修饰,从而变成常函数。
下面给出可执行代码示例:
/*----------头文件Circle.h-----------*/
#ifndef CIRCLE_H
#define CIRCLE_H
//声明一个类
class Circle {
public:
double radius;
Circle();//默认构造函数
Circle(double r);//有参构造函数
double getarea();//实例函数
};
#endif
/*----------源文件Circle.cpp-----------*/
#include"Circle.h"
//默认构造函数的实现
Circle::Circle() {
radius = 1.0;//这里需要初始值是因为radius默认私有
}
//有参构造函数的实现
Circle::Circle(double r) {
radius = r;
}
//实例函数的实现
double Circle::getarea() {
return(3.14 * radius * radius);
}
/*----------源文件main.cpp-----------*/
#include<iostream>
#include"circle.h"
using std::cout;
using std::endl;
//创建三个不同类型的重载函数print
/*由于按对象传递、按对象引用传递两者是通用的,所以要隐去一个
//直接用circle类型的对象
void print(Circle c) {
cout << c.getarea() << endl;
//注:由于以引用的形式作为形式参数可以避免从实际参数到形式参数的拷贝,所以更有优势
*/
//写引用类型的参数
void print(Circle& c) {
cout << c.getarea() << endl;
}
//写指针类型的参数
void print(Circle* c) {
cout << c->getarea() << endl;
}
int main() {
//用对象数组,注意不能用自动类型推导
Circle ca[]{ 1.0, 2.0, 3.0 };
print(ca[0]); //以对象的形式直接传参
print(ca[1]); //以引用的形式传参
print(ca + 2); //以指针的形式传参
//cin.get();
return 0;
}
同样的, 对象、对象引用、对象指针 也可以作为函数返回值。下面是 对象作为函数返回值 代码示例:
// class Object { ... };
Object f ( /*函数形参*/ ){
// Do something
return (*this);//返回(匿名)对象
}
// main() {
Object o = f ( /*实参*/ ); //得到一个对象
f( /*实参*/ ).memberFunction(); //o使用点运算符访问成员函数
下面是 对象引用 作为函数返回值的代码示例:
/* “邪恶”的用法示例 */
// class Object { ... };
Object& f ( /*函数形参*/ ){
Object o {args};
// Do something
return o; //邪恶的用法,因为返回时局部变量o会被销毁,返回的引用也会随之销毁
}
/* 允许的用法示例一 */
// class Object { ... };
Object& f ( Object& p, /*其它形参*/ ){
// Do something
return p;
}
// main() {
auto& o = f ( /*实参*/ ); //得到一个对象引用
f( /*实参*/ ).memberFunction(); //f使用点运算符访问成员函数
/* 允许的用法示例二——较为少见 */
// class Object { ... };
class X {
Object o;//o属于类X的成员,作为X的对象生成
Object f( /*实参*/ ){
// Do something
return o;
}
}
注意:应尽可能用const
修饰引用类型的函数返回值,除非你有特别目的(比如使用移动语义)。
const Object& f( /* args */) { }
下面是 对象指针 作为函数返回值的代码示例:
/* “邪恶”的用法示例 */
// class Object { ... };
Object* f ( /*函数形参*/ ){
// Do something
Object* o = new Object(args) // “邪恶”的用法,因为需要在函数外释放内存
return o;
}
// main() {
Object* o = f ( /*实参*/ );//这是一个对象指针,指向堆中
delete a;// 不能忘记释放内存
/* 允许的用法示例 */
// class Object { ... };
Object* f ( Object* p, /*其它形参*/ ){
// Do something
return p;
}
// main() {
Object* o = f ( /*实参*/ );//得到一个对象引用
f( /*实参*/ )->memberFunction();//f使用箭头运算符继续访问成员函数
注意:应尽可能用const修饰函数返回值类型和参数除非你有特别的目的(使用移动语义等)。
const Object* f(const Object* p, /* 其它参数 */) { }
网课中提到,传值、传址、传指针、传引用都是骗初学者的。C++中有意义的概念是传值和传引用。下面是两篇相关的文章:
- Differences between a pointer variable and a reference variable
- Difference between passing by reference vs. passing by value?
注意: 在为函数传参时,能用引用尽量不用指针。 除非有些情况下,例如使用 new 运算符,只能用指针。
- 引用更加直观,更少出现意外的疏忽导致的错误。
- 指针可以有二重、三重之分,比引用更加灵活。
- 这篇CSDN文章给出了C++中引用和指针的区别。
下面给出可执行代码的示例:
/*----------头文件Circle.h-----------*/
#pragma once
class Circle {
double radius;//私有
public:
Circle();
Circle(double r);
double getarea() const;
double getradius() const;
//Circle setradius(double r);//将对象作为返回值
//Circle& setradius(double r);//将对象引用作为返回值
Circle* setradius(double r);//将对象引用作为返回值
};
/*----------源文件Circle.cpp-----------*/
#include"Circle.h"
Circle::Circle() {
radius = 1.0;
}
Circle::Circle(double r) {
radius = r;
}
double Circle::getarea() const{
return(3.14 * radius * radius);
}
double Circle::getradius() const{
return (radius);
}
/* 将对象作为返回值,返回匿名对象 */
//Circle Circle::setradius(double r) {
// this->radius = r; //this指针表示本类
// //return (Circle{ radius });//直接构造一个匿名对象,作为返回值
// return(*this); //*this表示当前对象,与上一行等价
//}
/* 将对象引用作为返回值,返回对象本身 */
//Circle& Circle::setradius(double r) {
// this->radius = r;
// return(*this);//可以让主函数里c.setradius返回的是本身
//}
/* 将对象指针作为返回值,返回对象的指针 */
Circle* Circle::setradius(double r) {
this->radius = r;
return(this); //this表示当前对象的指针
}
/*----------源文件main.cpp-----------*/
#include<iostream>
#include"circle.h"
int main() {
Circle c{ 1.0 };
//以下两行将对象、对象引用作为返回值时运行
//std::cout << c.setradius(2.0).getarea() << std::endl;
//std::cout << c.setradius(3.0).setradius(4.0).getarea() << std::endl;
//以下两行将对象指针作为返回值运行
std::cout << c.setradius(2.0)->getarea() << std::endl;
std::cout << c.setradius(3.0)->setradius(4.0)->getarea() << std::endl;
//std::cin.get();
return 0;
}
注:上述代码的三个文件都要随着返回类型的不同同时更改。
4.4 抽象、封装、this指针
4.4.1 抽象与封装
数据域采用public:
的形式有很大的问题:数据可以被类外的方法篡改,从而使得类难于维护,易出现bug。如下代码示例:
class Circle {
public:
double radius;
//……
};
main() {
circle1.radius = 5; //类外代码可修改public数据
}
此时,就需要将类 封装 起来。将一些重要的变量变成私有private:
,从而只能在类内修改。示例如下:
class Circle {
private:
double radius;
public:
Circle();
//……
};
main() {
circle1.radius = 5; //报错!类外代码不可修改private数据
}
将类封装起来后,假如想读写私有参数,就需要调用类的 访问器 (Accessor/getter)与 修改器 (Mutator/setter)。也就是说,理想的封装好的类应该所有的数据成私有的,读/写操作都需要调用函数来完成。下面是其函数原型:
returnType getPropertyName() //get函数的一般原型
bool isPropertyName() //布尔型get函数的原型
void setPropertyName(dataType propertyValue)//set函数的原型
简而言之:
- 抽象: 提炼 目标系统中我们关心的 核心要素 的过程。
- 封装: 绑定数据和函数的语言构造块,以及 限制访问 目标对象的内容的手段。
4.4.2 成员作用域与this指针
数据成员(一般在private:
中)可被 类内所有 函数访问,并且数据域(private:
)与函数(public:
)可按任意顺序声明。
但假如成员函数中的局部变量与某数据域同名,由于局部变量优先级遵循“就近原则”,同名数据域就会在函数中被屏蔽,从而出现 同名屏蔽 (Hidden by same name)的问题。为避免混淆,不要在类中多次声明同名变量,除了函数参数。
要想在函数内访问类中被屏蔽的数据域,可以使用this
关键字。this
关键字是一种特殊的内建指针, 指向当前函数 ,可以引用当前函数的私有数据成员。this
指针的特点如下:
- 没有定义。
- 不许取地址,也就是不允许
&this
。
- 不能赋值。
- 能够指向对象,也就是当前对象。
于是利用this
指针,有下面两种避免同名屏蔽的方法:
class Circle {
public:
Circle();
Circle(double radius_){
radius = radius_; //方法一:不重名
}
private:
double radius;
public:
void setRadius(double radius){
this->radius = radius; //方法二:使用this指针
//第一个radius是类私有,第二个radius是函数形参。
}
//……
};
注意:
- 编码规范:11:若类的成员函数参数与私有成员变量名相同,那么参数名应加下划线后缀。
class SomeClass {
int length;
public:
void setLength( int length_ );
//……
}
4.5 [C++11]类数据成员的初始化
4.5.1 类成员的就地初始化
就地初始化,我的理解就是在类的声明中给私有变量直接(就地)赋给一个初值。在C++03标准中,只有 静态常量整型成员 才能 在类中 就地初始化:
class X {
static const int a = 7; // 允许
const int b = 7; // 错误: 非 static
static int c = 7; // 错误: 非 const
static const string d = "odd"; // 错误: 非整型
// ...
};
而在C++11标准中,非静态成员可以在它声明的时候初始化。就地初始化中文翻译最早出现在《深入理解C++11》;英文有许多种翻译,如 In-class initializer / default member initializer 。下面的示例代码展示了C++11类成员就地初始化的例子和规则:
class S {
private:
int m = 7; // ok, copy-initializes m
int n(7); // 错误:不允许用小括号初始化,编译器有可能误认为函数调用
std::string s{'a', 'b', 'c'}; // ok, direct list-initializes s
std::string t{"Constructor run"}; // ok
int a[] = {1,2,3}; // 错误:数组类型成员不能自动推断大小
int b[3] = {1,2,3}; // ok
// 引用类型的成员有一些额外限制,参考标准
public:
S() { }
};
4.5.2 构造函数的初始化列表
构造函数初始化列表 这个事情是针对 内嵌对象 来说的。如果类的数据域是一个对象类型,被称为对象中的对象(Object in Object),或者内嵌对象(Embedded Object)。 内嵌对象必须在被嵌对象的构造函数体执行前就构造完成。 下面的代码中,由于Time
缺少默认构造函数,且对象构造和赋值语句是两个不同的概念,不满足上述要求,所以会出现error:
。
class Time { /* Code omitted */ }
//假设类Time中只有有参构造函数,而缺少默认构造函数
class Action {
public:
Action(int hour, int minute, int second) {
time = Time(hour, minute, second);//赋值语句
//time对象应该在构造函数体(大括号)之前构造完成
}
private:
Time time;//对象构造
//time是Time的对象,是Action的内嵌对象
};
Action a(11, 59, 30);
要解决上述error:
,就需要 构造函数初始化列表 :
class Time { /* Code omitted */ }
//假设类Time中只有有参构造函数,而缺少默认构造函数
class Action {
public:
Action(int hour, int minute, int second)
:time{hour, minute, second} {} //构造函数的列表初始化
private:
Time time;//对象构造
//time是Time的对象,是Action的内嵌对象
};
Action a(11, 59, 30);
构造函数的初始化列表(下面代码第2行冒号后面的东西),需要使用列表初始化方法。下面给出构造函数的初始化列表示通用语法:
ClassName (parameterList)
: dataField1{value1}, dataField2{value2}
{
// Something to do
}
当然,虽然对于内嵌对象来说不等价,但是对于基础数据类型来说,初始化列表的方式和函数内定义的方式还是等价的,如下面代码所示:
/* 下面两种方法等价 */
/* 方法一:初始化列表 */
Circle::Circle() : radius{1}{
}
/* 方法二:构造函数内定义 */
Circle::Circle(){
radius = 1;
}
4.5.3 默认构造函数
默认构造函数 是可以 无参调用 的构造函数,既可以是定义为空参数列表的构造函数,也可以是所有参数都有默认参数值的构造函数。
/* 情况一:空参数列表的构造函数 */
class Circle1 {
public:
Circle1() { // 无参数
radius = 1.0; /*函数体可为空*/
}
private:
double radius;
};
/* 情况二:所有参数都有默认参数值的构造函数 */
class Circle2 {
public:
Circle2(double r = 1.0) // 所有参数都有默认值
: radius{ r } {
}
private:
double radius;
};
关于默认构造函数更详细的解释。
当对象类型成员/内嵌对象成员没有被显式初始化,就需要默认构造函数。该内嵌对象的无参构造函数会被自动调用;也可以在初始化列表中手动构造对象。若内嵌对象存在有参构造函数而没有无参构造函数,则编译器报错。
/* 自动调用无参构造函数 */
class X{
private:
Circle c1;
public:
X(){}
};
/* 初始化列表手动构造对象 */
class X{
private:
Circle c1;
public:
X():c1{}{
}
};
注:若类的数据域是一个对象类型(且它没有无参构造函数),则该对象初始化可以放到构造函数初始化列表中。说可以是因为C++11中还有就地初始化。
下面是默认构造函数的测试代码:
- 源文件
#include<iostream>
/* 内嵌对象:类Circle有默认构造函数 */
class Circle {
private:
double radius;
public:
//Circle(){ }//手动书写无参构造函数
Circle() = default;//要求编译器自动生成无参构造函数
Circle(double r) {
radius = r;
}
double getarea() {
return 3.14 * radius * radius;
}
};
/* 内嵌对象:类Square无默认构造函数 */
class Square {
private:
double side;
public:
Square() = delete;//指示编译器不准生成默认构造函数
//Square(double side) :side{ side } { };//构造函数初始化列表
Square(double s) {
side = s;
}
double getarea() {
return side * side;
}
};
class Combo {
public://这里违反了对象封装的规则,应该写get/set函数
Circle c;
Square s;
Combo() :s{ 1.0 }//这里必须使用构造函数初始化
{
s = { 8.0 };//这里必须使用赋值语句,不能初始化
};
};
int main() {
Combo co1{ };
std::cout << co1.c.getarea() << std::endl;//输出默认值
std::cout << co1.s.getarea() << std::endl;//输出64
std::cin.get();
return 0;
}
代码总结:对于内嵌对象,最内层的对象一定要都指出默认构造函数,有一个对象缺少默认函数,就必须在最外层对象的默认构造函数中做初始化列表。
老师建议: 由于C++初始化方法太多,所以平常就直接列表初始化,别的一概不用,除非有特别的理由。
4.5.4 成员的初始化次序
根据前述内容,初始化对象/类成员的方法有:就地初始化、构造函数初始化列表、在构造函数体中为成员赋值(注意,这个不是初始化,而是赋值)。下面是三种方法的比较,其中 执行次序 指代码运行顺序,这个过程中 优先级高 的初始化参数会覆盖掉优先级低的参数。
执行次序: 就地初始化 → \rightarrow → Ctor初始化列表 → \rightarrow → 在Ctor函数体中为成员赋值。
初始化/赋值优先级: 在Ctor函数体中为成员赋值 > Ctor初始化列表 > 就地初始化。
若一个成员同时有就地初始化和构造函数列表初始化,则就地初始化语句被忽略、不执行:
#include <iostream>
int x = 0;
struct S {
int n = ++x; // 就地初始化(default initializer)
S() { } // 使用就地初始化
S(int arg) : n{arg} { } // 使用成员初始化列表
};
int main() {
std::cout << x << '\n'; // 输出 0
S s1; //调用默认构造函数时一定会进行就地初始化
std::cout << x << '\n'; // 输出 1 (default initializer ran)
S s2(7); //调用有参构造函数是只是进行成员初始化列表
std::cout << x << '\n'; // 输出 1 (default initializer did not run)
}
- 一个例题:
struct X {
int x {8};
X() : x {10} {
x = 42;
}
};
X c;
//c.x为42
4.6 string类、[C++11]std::array类
4.6.1 C++字符串类
C++ 使用 string类 处理字符串,注意调用格式为 std::string
。若将重载函数和构造函数都算在内,那么string类的函数至少有100多个,下面是对主要函数的一个简单分类:
-
- 构造
-
- 追加
-
- 赋值
-
- 位置与清除
-
- 长度与容量
-
- 比较
-
- 子串
-
- 搜索
-
- 运算符
操作string对象中的字符串内容时,有时会用到“index”。很多string的函数接受如下两个数字参数:
index: 从index号位置开始
n: 之后的n个字符
比如说,字符串"Welcome to C and C++!"
中,"W"
是0号字符,令index = 6; n = 4
,那么从6号位置开始的4个字符为"e to"
。
- 创建 string 对象
/* 用无参构造函数创建一个空字串 */
string newString;
/* 由一个字符串常量或字符串数组创建string对象 */
string message{ "Aloha World!" };
char charArray[] = {'H', 'e', 'l', 'l', 'o', '\0'};//先创建字符数组
string message1{ charArray };//再将字符串数组作为名字传递给字符串对象
- 追加字符串
.append
/* 一系列的重载函数可以将新内容附加到一个字符串中 */
string s1{ "Welcome" };
s1.append( " to C++" ); // appends " to C++" to s1
cout << s1 << endl; // s1 now becomes Welcome to C++
string s2{ "Welcome" };
s2.append( " to C and C++", 3, 2 ); // appends " C" to s2
cout << s2 << endl; // s2 now becomes Welcome C
string s3{ "Welcome" };
s3.append( " to C and C++", 5); // appends " to C" to s3
cout << s3 << endl; // s3 now becomes Welcome to C
string s4{ "Welcome" };
s4.append( 4, 'G' ); // appends "GGGG" to s4
cout << s4 << endl; // s4 now becomes WelcomeGGGG
- 为字符串赋值
.assign
/* 一系列的重载函数可以将一个字符串赋以新内容 */
string s1{ "Welcome" };
s1.assign( "Dallas" ); // assigns "Dallas" to s1
cout << s1 << endl; // s1 now becomes Dallas
string s2{ "Welcome" };
s2.assign( "Dallas, Texas", 1, 3 ); // assigns "all" to s2
cout << s2 << endl; // s2 now becomes all
string s3{ "Welcome" };
s3.assign( "Dallas, Texas", 6 ); // assigns "Dallas" to s3
cout << s3 << endl; // s3 now becomes Dallas
string s4{ "Welcome" };
s4.assign( 4, 'G' ); // assigns "GGGG" to s4
cout << s4 << endl; // s4 now becomes GGGG
.at(index)
:返回当前字符串中index位置的字符。.clear()
:清空字符串。.erase(index, n)
:删除字符串从index开始的n个字符。.empty()
:检测字符串是否为空。
string s1{ "Welcome" };
cout << s1.at(3) << endl; // s1.at(3) returns c
cout << s1.erase(2, 3) << endl; // s1 is now Weme
s1.clear(); // s1 is now empty
cout << s1.empty() << endl; // s1.empty returns 1 (means true)
- 比较字符串
.compare()
函数用于比较两个字符串。它与C语言中的strcmp()
函数很像。
string s1{ "Welcome" };
string s2{ "Welcomg" };
cout << s1.compare(s2) << endl; // returns -2
cout << s2.compare(s1) << endl; // returns 2
cout << s1.compare("Welcome") << endl; // returns 0
- 获取子串
.at()
函数用于获取一个单独的字符,.substr()
函数则可以获取一个子串。
string s1{ "Welcome" };
cout << s1.substr(0, 1) << endl; // returns W; 从0号位置开始的1个字符
cout << s1.substr(3) << endl; // returns come; 从3号位置直到末尾的子串
cout << s1.substr(3, 3) << endl; // returns com;从3号位置开始的3个字符
- 搜索字符串
.find()
函数可以在一个字符串中搜索一个子串或者一个字符。
string s1{ "Welcome to C++" };
cout << s1.find("co") << endl; // returns 3; 返回子串出现的第一个位置
cout << s1.find("co", 6) << endl; // returns -1 从6号位置开始查找子串出现的第一个位置
cout << s1.find('o') << endl; // returns 4 返回字符出现的第一个位置
cout << s1.find('o', 6) << endl; // returns 9 从6号位置开始查找字符出现的第一个位置
- 插入和替换字符串
.insert()
:将某个字符/字符串插入到当前字符串的某个位置。
.replace()
:将本字串从某个位置开始的一些字符替换为其它内容。
string s1("Welcome to C++");
s1.insert(11, "Java and ");//第11个字符之后
cout << s1 << endl; // s1 becomes Welcome to Java and C++
string s2{ "AA" };
s2.insert(1, 4, 'B'); //在1号位置处连续插入4个相同字符
cout << s2 << endl; // s2 becomes to ABBBBA
string s3{ "Welcome to Java" };
s3.replace(11, 4, "C++"); //从11号位置开始向后的4个字符替换掉。注意'\0'
cout << s3 << endl; // returns Welcome to C++
- String Operators (字符串运算符)
Operator | Description |
---|---|
[ ] | 用数组下标运算符访问字符串中的字符 |
= | 将一个字符串的内容复制到另一个字符串 |
+ | 连接两个字符串得到一个新串 |
+= | 将一个字符串追加到另一个字符串末尾 |
<< | 将一个字符串插入一个流 |
>> | 从一个流提取一个字符串,分界符为空格或者空结束符 |
==, !=, <, <=, >, >= | 用于字符串比较 |
string s1 = "ABC"; // The = operator
string s2 = s1; // The = operator
for (int i = s2.size() - 1; i >= 0; i--)
cout << s2[i]; // The [] operator
string s3 = s1 + "DEFG"; // The + operator
cout << s3 << endl; // s3 becomes ABCDEFG
s1 += "ABC";
cout << s1 << endl; // s1 becomes ABCABC
s1 = "ABC";
s2 = "ABE";
cout << (s1 == s2) << endl; // Displays 0
cout << (s1 != s2) << endl; // Displays 1
cout << (s1 > s2) << endl; // Displays 0
cout << (s1 >= s2) << endl; // Displays 0
cout << (s1 < s2) << endl; // Displays 1
cout << (s1 <= s2) << endl; // Displays 1
下面给出string类测试代码:
- 源文件
#include<iostream>
#include<string>//进行字符串操作
using std::cin;
using std::cout;
using std::endl;
using std::string;
int main() {
//创建字符串
//string s{ "hello" };
string s = "hello";
cout << s << endl;//输出"hello"
//清除字符串
s.clear();
cout << s << endl;//输出空
//用数组为字符串赋值
char arr[]{ " world" };
s += arr;
cout << s << endl;//输出" world"
//assign重新赋值
s.assign("hello world!");
cout << s << endl;//输出"hello world"
//append,加空格的两种方法
s.append(" "); //加1个空格
s.append(5, ' '); //加5个空格
s.append("!"); //加"!"便于观察
cout << s << endl; //输出"hello world !"
//insert开头插入空白
s.assign("hello world! ");
s.insert(0, " ");
cout << s << endl; //输出" hello world "
//移除字串前面的空白
s.erase(0, s.find_first_not_of(" \t\r\n"));
cout << s << endl; //输出"hello world "
//移除字串后面的空白
s.erase((s.find_last_not_of(" \t\r\n") + 1));
cout << s << endl; //输出"hello world"
//把字符串转化为一个整数
s.assign("1024");
int x = std::stoi(s);//string to int
/* 输出方法一 */
//cout << "x = ";
//for (auto x : s) {
// cout << x;
//}
//cout << "\n"; //输出"x = 1024"
/* 输出方法二 */
cout << "x = " << x << endl;//输出"x = 1024"
//把一个整数转化为字符串
string s2 = std::to_string(x);
cout << s2 << endl; //输出"1024"
//cin.get();
return 0;
}
4.6.2 C++11的数组类
C风格的数组 (C Style Array),也就是 C++原生数组 (C++ raw array)int arr[ ] = { 1, 2, 3 };
,存在一些问题:
- arr 可能会退化为指针:
void f(int a[]) { std::cout << sizeof(a)/sizeof(a[0]); }
中,a
会被视为指针,报错。 - arr 不知道自己的大小:
sizeof(arr)/sizeof(arr[0])
报错。 - 两个数组之间无法直接赋值:
array1 = array2;
中数组名字相当于首地址,报错。 - 不能自动推导类型:
auto a1[] = {1,2,3};
报错。
进而针对上述缺点,提出 C++风格的数组 (C++ Style Array),C++数组类是一个模板类,可以容纳任何类型的数据(这点和C语言数组相同),调用格式和使用限制如下:
/* C++风格数组调用格式 */
#include <array>
std::array<ArrayType, ArraySize> ArrayName;
std::array<ArrayType, ArraySize> ArrayName { Value1, Value2, …};
/* 限制与C风格数组相同 */
std::array<int , 10> x; //限制一:数组只能存储同类型数据。
std::array<char , 5> c{ 'H','e','l','l','o' }; //限制二:数组大小一旦确定就不能更改。
C++风格的数组具有以下优点:
- 是一个容器类,所以有迭代器(可以认为是一种用于访问成员的高级指针)
- 可直接赋值
- 知道自己大小:
size()
- 能和另一个数组交换内容:
swap()
- 能以指定值填充自己:
fill()
- 取某个位置的元素( 做越界检查) :
at()
进一步的,C++17引入了一种新特性,可以对类模板的参数进行推导(学完模板才能看懂这句话)。也就是说调用std::array
时不需要声明数组类型和数组大小了,编译器会自动进行 类型推导 (Type Deduction)。下面是代码示例:
std::array a1 {1, 3, 5}; // 推导出 std::array<int, 3>
std::array a2 {'a', 'b', 'c', 'd'}; // 推导出 std::array<char, 4>
最后给出std::array
的成员函数:
元素访问 | 容量 | ||
---|---|---|---|
at | 访问指定的元素,同时进行越界检查 | empty | 检查容器是否为空 |
operator[ ] | 访问指定的元素 | size | 返回容纳的元素数 |
front | 访问第一个元素 | max_size | 返回可容纳的最大元素数 |
back | 访问最后一个元素 | 迭代器 | (可以理解成高级指针) |
data | 返回指向内存中数组第一个元素的指针 | beginc begin | 返回指向容器第一个元素的迭代器 |
操作 | endc end | 返回指向容器尾端的迭代器 | |
fill | 以指定值填充容器 | rbeginc rbrgin | 返回指向容器最后元素的逆向迭代器 |
swap | 交换内容 | rendc rend | 返回指向前端的逆向迭代器 |
注:C++11风格数组和C++原生数组在性能上相似,但是由于C++11风格数组具有更好的特性,所以推荐使用std::array
替换C++原生数组。
下面给出C++风格数组测试代码:
- 源文件
#include<iostream>
#include<array>//使用数组类
#include <algorithm>//使用排序sort函数
using std::cin;
using std::cout;
using std::endl;
using std::array;
//print将数组元素依次打印出来
void print(array<int, 3>& a) {
for (auto x : a) {//基于范围的for
cout << x << " ";
}
cout << endl;
}
//search查找数组中的元素
int search(array<int, 3>& a, int token) {
bool isExist{ false };
int i{ 0 };
for (int element : a) {
if (element == token) {
isExist = true;
break;
}
i++;
}
if (isExist) {
return i;
}
else {
return -1;
}
}
int main() {
//创建数组
array a1 = { 1,2,3 };//类型推导成 array <int, 3>
cout << "a1: ";
print(a1);
//为数组赋值
a1 = { 4,5,6 };//C++原生数组是不可以的
cout << "a1: ";
print(a1);
//交换数组
array a2{ 100,200,300 };
cout << "a1: ";
print(a1);
a1.swap(a2);
cout << "交换后a1: ";
print(a1);
cout << "交换后a2: ";
print(a2);
//求数组大小
cout << "a1大小为:" << a1.size() << endl;
cout << "a1大小为:" << a1.max_size() << endl;
//编写search函数,在数组中查找一个值
cout << "search 100:" << search(a1, 100) << endl;
cout << "search 400:" << search(a1, 400) << endl;//-1表示没有找到
//sort排序
a1 = { 8,7,9 };
std::sort(a1.begin(), a1.end());//默认从小到大
cout << "a1排序后: ";
print(a1);
//二维数组
std::array<std::array<int, 2>, 3> a3 { 1, 2, 3, 4, 5, 6 };
for (int i = 0;i < 3;i++) {
for (int j = 0;j < 2;j++) {
cout << a3[i][j] << " ";
}
cout << "\n";
}
//cin.get();
return 0;
}
- 结果
a1: 1 2 3
a1: 4 5 6
a1: 4 5 6
交换后a1: 100 200 300
交换后a2: 4 5 6
a1大小为:3
a1大小为:3
search 100:0
search 400:-1
a1排序后: 7 8 9
1 2
3 4
5 6