类:简单来说就是数据和它的操作的一种封装,内部提供接口函数
1.“ 定义 ”在类内部的函数是隐式inline的。
2.this
成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。用请求该函数的对象的地址来初始化this。
仍何对类成员的访问都被看作this的隐式引用。
std::string isbn( ) { return bookNo; }
std::string isbn( ) { return this->bookNo; }
//二者是等价的
this是一个常量指针,不允许改变this中保存的地址
3.const 成员函数
this是一个指向类类型非常量版本的常量指针。
Sales_data * const this
所以我们不能把this绑定到一个常量对象上。所以我们不能在一个常量对象上调用普通的成员函数。
const 常量成员函数:C++允许把const关键字写在函数的参数列表后面表示this是一个指向常量对象的指针。
#include <iostream>
#include <string>
using namespace std;
class Sales_data
{
//isbn()函数后面添加const,实际上是修饰this指针,表示
//this是一个指向常量的指针。所以isbn()函数内不会修改
//this所指向的内容,当然this可以省略.
std::string isbn() const { return this->bookNo; }
private:
std::string bookNo; //图书编号
unsigned units_sold = 0; //销售单价
double revenue = 0.0; //总销售额
};
!常量对象,以及常量对象的指针或者引用都只能调用常量成员函数。
一个const 成员函数如果以引用的方式返回*this,那么它的返回类型是常量引用。
来分析一个问题,static成员在常量成员函数中能改变吗?
答案是可以的。
static成员存在于内存中的静态区,和类的普通成员存在的位置不一样,其次来看看函数参数列表后面加上const的情况
在普通成员函数中,this指针是一个常量指针,我们访问变量等都是通过this指针访问,一般省略,this指针形式类似 类型 * const this
当我们变成常量成员函数的时候,this指针变成了 const 类型 * const this ,所以我们不能修改常量成员函数里的变量,因为this是指向常量的常量指针。
但是static变量和普通变量却不再一块区域存储,那么当然不受this指针的控制,!所以在常量成员函数里面可以修改static成员变量。
4.定义类相关的非成员函数
属于类的接口组成部分,但不属于类的本身。
I/O类不能被拷贝。
执行输出的函数应该减少对格式的控制,这样可以确保由用户代码来决定是否换行。
5.构造函数 :构造函数的唯一目的就是为成员赋初值。
定义:类通过一个或几个特殊的成员函数来控制其对象初始化的过程。
只要对象被创建,就会执行构造函数。
默认构造函数:类通过一个特殊构造函数来控制默认初始化过程。默认构造函数无需实参
合成默认构造函数:我们没有定义构造函数时,编译器为我们定义的构造函数。
class Sales_item
{
Sales_item( ) = default;
Sales_item( const std::string &s) : bookNo(s) { } //若不是引用的话,可以std::string s,是&必须加const,保证不会改变s。
Sales_item( const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p) { }
private:
std::string bookNo;
unsigned units_sold;
double revenue;
}
// Sales_item( ) = default; c++11标准中,如果我们需要默认的行为,那么可以通过早参数列表后面写上 = default 来要求编译器生成构造函数。
c++ 11 类内初始值
class c
{
c( ): { }
private:
int i = 10; //类内初始值
}
通常情况下,构造函数使用类内初始值不失为一种好的选择,因为只要这样的初始值存在我们就能确保为成员赋予了一个正确的值。
类内初始值只在没有任何构造函数的情况下才起作用
在类外部定义构造函数
Sales_data::Sales_data( std::istream &is)
{
read(is, *this); //read函数从is中读取一条信息存到this中去。
}
接下来来分析一个实例类
class Sales_data
{
public:
Sales_data() = default;
Sales_data(std::string s):bookNo(s), units_sold(0), revenue(0.0) { }
Sales_data(std::string s, unsigned n, double p): bookNo(s), units_sold(n), revenue(p) { }
std::string isbn() const { return bookNo; }
Sales_data& combine(const Sales_data &rhs);
double avg_price() const { return revenue/units_sold; }
Sales_data& operator=(const Sales_data &rhs);
friend ostream& print(ostream& os, const Sales_data& item);
friend istream& read(istream& is, Sales_data& item);
friend Sales_data add(const Sales_data&item1, const Sales_data&item2);
private:
std::string bookNo = "w"; //编号
unsigned units_sold = 0; //销售数量
double revenue = 0; //总销售额
//double price; //单价
};
int main()
{
Sales_data item1;
Sales_data item2("wangweihao");
Sales_data item3("wangweihao", 10, 10000);
print(cout, item1) << endl;
print(cout, item2) << endl;
print(cout, item3) << endl;
return 0;
}
上面的类运行成立,
情况1
去掉 Sales_data( ) = default;
报错,没有定义默认构造函数,当我们自己定义了其他任何一种构造函数时,编译器就不会帮我们合成默认构造函数。
情况2
当我们不提供类内初始值(c++11)时,默认构造函数不会帮我们初始化内置类型,如int, double等等。
情况3
使用流初始化的构造函数
Sales_data(std::istream &is)
{
read(is, *this);//read从is中读取一条信息存到this对象中。
}
main()
{
Sales_data item(cin); //就会从流中读取一条信息。
}
简单的使用流初始化。
int main()
{
Sales_data total(cin);
while(1)
{
Sales_data item(cin);//不能写到while循环里,会报错
if(total.isbn() == item.isbn())
total.combine(item);
else
{
print(cout, total);
total = item;
}
}
print(cout, total);
return 0;
}
6.访问控制和封装
使用访问控制符增加类的封装性
public: 成员可以在整个程序内被访问,public定义类的接口
private: 成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节
我们也可以使用struct 和 class 定义类( 它俩的唯一区别就是默认的访问权限 )
struct 在第一个限定符之前默认是public的
class 在第一个限定符之前默认是private的
7.友元
类允许其他类或者函数访问它的非公有成员,方法是令其他类或函数成为它的友元
如果想把一个函数作为友元,只需要添加一条以friend关键字开头的函数声明即可
friend std::istream& read(std::istream &is, Sales_data &rhs);
//注意如果read函数在其他.h文件中定义,我们必须在用它的类的.h文件中先声明它在设置为友元
一般来说,最好在类定义开始或结束前的位置集中声明友元
类还可以把其他的类定义成友元,也可以把其他类(之前定义过的)的成员函数定义为友元。
此外,友元函数能定义在类的内部,这样的函数是隐式内联的
如果类指定的友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。
令成员函数作为友元
!当把一个成员函数声明为友元时,我们必须明确指出该成员函数属于哪个类
明确组织结构!
。。。
重载函数作为友元,尽管名字相同,但是他们依然是不同的函数。要分别对每一个函数进行声明
就算在内部定义友元函数,我们也应该在外部声明它使得它可见。
8.可变数据成员
用mutable修饰的成员永远不会是const ,即使出现在const函数中
#include <iostream>
class A
{
public:
void b(void)const
{
i++;
std::cout << m << std::endl;
std::cout << i << std::endl;
}
private:
int m = 0;
mutable int i = 0;
static int k;
//static int k = 0; error:只能定义不能初始化static成员
//const static int k = 0 正确:但是只能是int型, 其他double,string都不行。
};
int main()
{
A a;
for(int j = 0; j < 10; j++)
a.b();
}
可在const 函数中修改mutable成员。
但是从上面的函数又引出来了一个问题
当我把
mutable int i = 0;
替换成
mutable static int i =0;
会报错:
245.cpp:32:32: error: conflicting specifiers in declaration of ‘i’
245.cpp: In member function ‘void A::b() const’:
245.cpp:25:13: error: ‘i’ was not declared in this scope
因为类的对象实例化是在一个指定的区域
再举个例子,说明类中static变量怎么用
#include <iostream>
#include <string>
class T
{
public:
T():i(10) { }
void A(void)
{
i++;
std::cout << i << std::endl;
std::cout << j << std::endl;
}
private:
int i;
static int j;
};
int T::j = 10;
int main()
{
T t;
t.A();
}
静态变量j是所有类的实例化对象所共用的,一般初始化我们在main函数之前。
注意:
不带static关键字
必须带上类型和属于哪个类的作用域符号。
static变量存在的区域是全局区,它和类的实体不在一个区域,所以会报错,我们在类中定义static成员不能让类中的函数等访问。
别人博客的一段话
说完了static成员后,我们再来看看static成员函数,static成员是类的组成部分并不是任何对象的组成部分,
因此,static成员函数没有this指针。我们知道,一般而言,类中的成员函数具有一个附加的隐含实参,即指向
该类对象的一个指针。这个隐含实参命名为this。因为static成员函数不是任何对象的组成部分,所以static
成员函数就没有this形参了。由于成员函数声明为const说明该成员函数不会修改该成员函数所属的对象,所以
static成员函数不能声明为const。为什么呢?因为static成员函数不是任何对象的组成部分。static成员函数
可以直接访问所属类的static成员,但是不能直接使用非static成员函数!也不能访问static const 类型的成员!
总之static关键字修饰的在内存中存储是在静态区也就是全局区,而新建的类是在栈区。
静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的。在函数的返回类型前加上static关键字,函数即被定义为静态函数。静态函数与普通函数不同,它只能在声明它的文件当中可见,不能被其它文件使用。
9.我们希望类开始时总是有默认初始值, 在c++11中,最好的方式就是把这个默认值声明成一个类内初始值。
类内初始值必须以=号或者花括号表示。
#include <iostream>
#include <vector>
#include <string>
class Window_mgr
{
private:
std::vector<std::string>ivec{"hello"};
//std::vector<std::string>ivec = "hello";
};
int main()
{}
10.返回*this的函数
myScreen.mov(4,0).set('c');
这句话的含义是mov移动到(4,0)位置设置此出的字符为'c'。
如果函数mov返回的是非引用类型,代码将不会正确执行,因为此时返回的是*this副本返回
以副本返回输出有可能也是副本,不要错误的认为真实的就改变了。
Screen& mov(pos h, pos w)
{
cursor = h*width + w;
return *this;
}
一个const 成员函数以引用的形式返回的*this,那么它的返回类型也是常量引用。
this指针的好处
在类中区分类的变量和类方法的变量
#include <iostream>
class A
{
public:
void fun()
{
int a = 5;
std::cout << a << std::endl; //区分局部变量和成员变量,不过我们尽量不要这样做,可以重新起个名字
std::cout << this->a << std::endl;
}
private:
int a = 10;
};
int main()
{
A n;
n.fun(); //输出结果是5和10
return 0;
}
一个类的所有实例调用的方法在内存中只有一份拷贝,尽管可能存在多个对象,
每个对象在内存中都有一份拷贝,this允许方法为不同的对象工作,每次调用
方法时, this变量会变成引用该方法的特定的实例,方法代码接着会和特定的对象相关联。
11.类的作用域
<1.一个类就是一个作用域的事实能够很好的解释为什么当我们在类的外部定义成员函数时必须同时提供类名和函数名。
<2.返回类型或者任何一个参数类型是类中的,都要指明它属于哪个类。
<3.编译器处理完类中全部的声明参会处理成员函数的定义,因为成员函数体在整个类可见后才会处理,所以他能使用类中定义的任何名字
类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。
<4.可以通过::来访问外部作用域被隐藏的变量。!不过我们应该尽量保证不要隐藏外部作用域可能用到的名字。
12.名字查找与类的作用域
<1.首先,在名字所在的块中寻找其声明语句,只考虑在名字使用之前出现的声明
<2.如果没找到,继续查找外层作用域
<3.如果最终没有找到匹配的报错
类的定义分两步处理
<1.首先编译成员的声明
<2.直到类全部可见后才编译函数体。
13.构造函数在探究
<1 构造函数的初始值列表。
string foo = "hello world";
string bar;
bar = "hello ,world";
这两个的区别就是foo直接赋值而避免了构造函数初始化
bar先使用构造函数初始化话了以后在使用赋值。
<2 构造函数的初始值不可少
#include <iostream>
class fun
{
public:
void func(){ std::cout << "func" << std::endl; }
fun(): k(1) { }
private:
const int i; //提供默认函数初始化,但是一旦定义了型如上面的构造函数就会报错,因为不会默认合成构造函数,引用和const就没有被初始化了
int &j;
int k;
};
int main()
{
int i = 10;
int &r = i; //引用和const必须要初始化。
//int &k;
//const int n;
}
我们初始化const或者引用类型的数据成员唯一的机会就是通过构造函数初始值。
所以!使用构造函数初始值。
<3 成员初始化顺序
成员的初始化顺序与他们在类中定义的顺序一样
class A
{
public:
A( int val ) : j(val), i(j); //报错,先初始化i。
private:
int i;
int j;
}
最好令构造函数初始值的顺序与成员声明的顺序一致,而且如果有可能的话,尽量避免使用某些成员初始化其他成员。
<4 不能两个构造函数都使用默认实参,会产生二义性。
Sales_data(std::string s = " "):bookNo(s) { std::cout << "Sales_data(std::string s = " ")" << std::endl; }
Sales_data(std::string s, unsigned cnt, double rev) : bookNo(s), units_sold(cnt), revenue(rev)
{ std::cout << "(std::string s, unsigned cnt, double rev) : " << std::endl; }
Sales_data(std::istream &is = std::cin) { read(is, *this); std::cout << "std::istream &i = cin" << std::endl;}
Sales_data(std::string s = " ")会和Sales_data(std::istream &is = std::cin)产生二义性。
因为一但定义
Sales_data a;
不知道用那一个构造函数来初始化。
<5 c++11 委托构造函数
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数
加入委托的构造函数函数体里面包含有代码的话,先执行这些代码,然后控制权才会交还给委托者的函数体
列子
#include <iostream>
#include <string>
class Sales_data
{
public:
Sales_data(std::string bn, unsigned us, double re): bookNo(bn), units_sold(us), revenue(re)
{ std::cout << "委托Sales_data(std::string bn, unsigned us, double us) \n执行函数体" << "\n"<< std::endl;}
Sales_data(): Sales_data("", 0, 0) //委托三参版本
{ std::cout << "归还Sales_data():Sales_data("", 0, 0) \n执行函数体" << "\n" << std::endl; }
Sales_data(std::string s): Sales_data(s, 0, 0) //委托三参版本
{ std::cout << "归还Sales_data(std::string):Sales_data(s, 0, 0) \n执行函数体" << "\n" << std::endl;}
Sales_data(std::istream &is) : Sales_data() //委托默认版本,默认版本在委托三参版本。
{
read(is, *this);
std::cout << "执行Sales_data(std::istream &is) \n执行函数体" << "\n" << std::endl;
}
friend std::istream& read(std::istream& is, Sales_data& item);
private:
std::string bookNo;
unsigned units_sold;
double revenue;
};
std::istream& read(std::istream& is, Sales_data& item)
{
is >> item.bookNo >> item.units_sold >> item.revenue;
return is;
}
int main()
{
std::cout << "a" << std::endl;
Sales_data a;
std::cout << "b" << std::endl;
Sales_data b("string");
std::cout << "c" << std::endl;
Sales_data c(std::cin);
}
运行结果
C++没有提供让一个构造函数去委托另一个构造函数执行构造操作的机制。这意味着不能(或不提倡)
使用缺省参数,类的维护者不得不编写并维护多个构造函数。这会导致源代码和目标代码的重复,降低
了可维护性(由于可能引起不一致性),有时还会导致代码膨胀。
14.隐式的类类型转换
我们也能为类定义隐式的转换机制,如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,
有时把这种构造函数称为转换构造函数。
class Sales_data
{
public:
Sales_data() = default;
Sales_data(std::string s):bookNo(s), units_sold(0), revenue(0) { }
Sales_data(std::istream &is):{ read(is, *this);}
};
Sales_data s1;
string s = "hello";
s1.combine(s);
成立,因为存在只传参string s的构造函数。
那么s1.combine(s);中的s会调用Sales_data的只传参数s的版本的构造函数,那么s1.combine(s)的s就会变成一个Sales_data对象,所以可以调用combine函数。
我们可以抑制构造函数定义的隐式转换,既不能通过穿参数s隐式构造一个Sales_data对象。
通过将构造函数声明为 explicit 加以阻止
class Sales_data
{
public:
Sales_data() = default;
explicit Sales_data(std::string s):bookNo(s), units_sold(0), revenue(0) { }
explicit Sales_data(std::istream &is):{ read(is, *this);}
};
7.49题
Sales_data combine(Sales_data);
Sales_data combine(Sales_data &);
Sales_data combine(const Sales_data &);
s是一个string 类型的字符串
调用i.combine(s);
Sales_data combine(Sales_data);
正确,因为Sales_data是一份副本,会执行构造函数,所以存在隐式转换
Sales_data combine(Sales_data &);
错误,因为Sales_data是引用,不会执行构造函数,所以不存在隐式转换,类型不匹配,错误。
Sales_data combine(const Sales_data &);
正确,想向const Sales_data &传参数的时候,实际上是先创建一个temp值, 然后让const 修饰这个temp,所以也存在建立的过程
15.static
<1.静态成员函数可以访问静态成员变量和和静态成员函数
<2.非静态成员函数也可以访问静态成员变量和和静态成员函数
<3.静态成员函数没有this指针,无法访问属于类对象的非静态成员变量和非静态成员函数
<4.由于没有this指针的额外开销,因此静态成员函数与类的非静态成员函数相比速度上会有少许的增长
<5.静态成员函数/变量属于整个类,没有this指针,该类的所有对象共享这些静态成员函数/变量
<6.非静态成员函数/变量属于类的具体的对象,this是缺省的
<7.静态成员变量在类内声明,且必须带static关键字;在类外初始化,且不能带static关键字
<8.静态成员函数在类内声明,且必须带static关键字;在类外实现,且不能带static关键字