C++学习笔记1--类
C++书籍推荐
- 语言:C++ Primer, <the C++ programming lauguage>
- 经验:effective C++ 系列
- 标准库:the C++ standard library STL 源码解析
本文主要介绍C++中的类的基础,通过complex和string两个类介绍类的基础构成和规范写法。主要包括构造函数,拷贝、赋值构造函数,穿插一些小知识点,比如头文件、单例模式、内存管理等知识。
头文件和类的声明
C vs C++
在C中,所有的函数都可以访问data。在C++中,将数据和函数封装在一起,只有这个类中的函数才能访问这个类中的数据。
- Object Based: 单一class的设计
- Object Oriented: 多重classes之间的设计。
classes 的两个经典分类:
- class without pointer members (complex)(多半不需要析构函数)
- class with pointer members (string)
头文件
为了防止头文件的重复包含,要定义下面的防卫式声明:
#ifndef __COMPLEX__
#define __COMPLEX__
#endif
ifndef和pragma once的区别
ifndef的方式受C/C++语言标准支持。它不光可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含。
当然,缺点就是如果不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,编译器却硬说找不到声明的状况——这种情况有时非常让人抓狂。
由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式。
pragma once一般由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件。你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件。
其好处是,你不必再费劲想个宏名了,当然也就不会出现宏名碰撞引发的奇怪问题。大型项目的编译速度也因此提高了一些。
对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名碰撞引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。
文件结构
- 类的声明(模板)
- 类的实现
- 前置声明
inline 函数
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说,inline可以带来一定的效率提升,而且和C时代的宏函数相比,inline 更安全可靠。
内联函数和宏的区别
- 内联函数在运行时可调试,而宏定义不可以;
- 编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
- 内联函数可以访问类的成员变量,宏定义则不能;
- 在类中声明同时定义的成员函数,自动转化为内联函数。
内联函数的适用情况
一个函数不断被重复调用。
函数只有简单的几行,且函数不包含for、while、switch语句。
inline函数对编译器来说只是一个建议,编译器可以选择忽略。
构造函数
class complex
{
complex (double r=0, double i=0)
:re(r), im(i) //1
{} //2
private:
double re, im;
};
在1和2位置初始化的区别:在1是初始化,在2是赋值。
类的初始化尽量使用初始化列表。
构造函数的重载
1. complex (double r=0, double i=0)
:re(r), im(i) //1
{}
2. complex ():re(0),im(0) {}
1和2不可以同时存在,假如有代码complex c1; complex c2();
,两个都不带有参数,构造函数1和2都不带有参数,都可以调用,所以这两个不能同时存在。
单例模式
构造函数通常不会放在private里面,因为在创造对象的时候会调用构造函数,如果构造函数是private,就不能被外界调用。但是下面的单例模式情况例外:
class A {
public:
static A& getInstance();
setup () {...}
private:
A();
A(const A& rhs);
static A a;
...
};
A& A::getInstance()
{
return a;
}
A::getInstance().setup();
首先private里面有一个自己a,然后构造函数也是私有的,也就是不允许外界构造。类A只有一个对象a。那么外界如何调用呢,通过A::getInstance返回的a,然后调用setup进行其他操作。
但是这种写法,当外界不调用的时候a仍然存在。
class A {
public:
static A& getInstance();
setup () {...}
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}
A::getInstance().setup();
只有有人调用的时候a才会存在,并且只有一份。
常量成员函数
double real() const { return re; }
如果函数不改变数据,在函数后面加上const关键字。
如果函数没有加const关键字,当有一个常量类对象const complex c1(2,1)
想调用c1.real()
时,用户指定c1不会改变,但是函数却没有加const,意思是可能会改变c1的值,这时就会产生矛盾。具体见下面const一章。
参数传递 value/reference(to const)
参数传递尽量使用引用,返回值传递也尽量使用引用
值传递就是把一整包数据传递过去,通过栈。数据多大就传多大。
引用只是传递指针,速度比较快。
有时候只希望传递效率快,但是不希望传递的值被改变,这时候需要加const。
不可以使用引用的情况
函数的结果传递有两种情况,第一种是生成一个临时变量,比如return a+b;
;第二种是返回东西已经有空间存放。
此时第一种情况不可以返回引用,因为当函数结束的时候,这个对象就会消失,传递的引用是坏的。(参考操作符重载-非成员函数的例子)
当使用引用时,传递者无需考虑接受者是以什么形式接收
参考操作符重载一章。
指针和引用的区别参考下面的博客:
https://www.cnblogs.com/x_wukong/p/5712345.html
友元
友元函数打破封装,可以直接拿类里面的数据,效率比较高。
int func(const complex& param)
{ return param.re + param.im; }
c2.func(c1);
相同class 的各个objects互为friend
操作符重载
操作符实际上也是一种函数,C++允许重载。
成员函数
c2+=c1;
+=重载使用成员函数是因为需要调用类的成员变量。
符号+=作用在左边的对象,编译器寻找左边对象中有没有+=这个函数。
inline complex& //1
__doapl(complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex& //2
complex::operator +=(const complex& r)
{
return __doapl(this,r);
}
所有的成员函数都隐含有一个this指针,在这里把c2传给this指针,c1传给r。__doapl(do assignment plus)封装成一个函数便于其他函数调用。
当使用引用时,传递者无需考虑接受者是以什么形式接收
看上面的代码1,return *ths
但是返回参数写的是complex&
。
传递一个object,接受者是complex&
,如果对方也是引用,ok。如果对方用value也可以。
允许连续赋值
在语句2地方返回引用使得用户可以使用c3+=c2+=c1
这种用法。假如函数返回的是void,那么c3+=(void)
是不可以的。
问题:返回值是complex类型可以连续赋值吗?
非成员函数
inline complex
operator + (const complex& x, const complex& y)
{
return complex(real(x)+real(y),
imag(x)+imag(y));
}
inline complex
operator + (const complex& x, double y){...}
inline complex
operator + (double x, const complex& y){...}
这三种重载函数允许复数+复数、复数+实数、实数+复数。
注意这里的函数不能返回引用,因为他们的返回值一定是个local object。
complex();–typename();的用法就是创建了一个临时对象。在下一行生命结束。
问题:complex(); complex c3();complex c3的区别
问题:这里的double类型为什么不用const呢?
上面函数后面加const是因为怕使用者创建const对象调用非const函数。参数上加const是因为防止被修改,如果不想被修改就加上const。这里不加是因为没有加的必要,从第一个原因上来说,加入调用者创建const double y,也能传给double y。 另外这里很确定不会改变y的值。
const也是函数签名的一部分,也就是说允许加const和不加const的函数同时存在。
<<运算符重载
只能写成非成员函数。如果写成成员函数,那么使用时成了c1<<cout,这是不可以的。
ostream&
operator <<(ostream& os, const complex& x)
{
return os << '(' << real(x) << ',' << imag(x) << ')';
}
调用:
cout<<c1;
- 参数的确定:第一个参数是<<左边的对象,也就是cout的类型。第二个参数是<<右边的对象。
- 要不要const?每次输出的时候实际上在改变os的状态,所以不能加const。
- 返回值类型。为了能够连续输出,还需要作为左值,所以返回值还是ostream类型。
实现complex类需要注意的问题
- +=运算符只重载了一次
complex& operator +=( const complex& y)
,5被转化为complex类。 对应的调用注意加括号。cout<<(c2+=5)<<endl;
如果不实现为友元函数的话,则应该在类内实现,因为需要调用this指针。 - <<的重载函数要写成std::ostream,并且包含头文件#include
. 另外ostream不允许在数据声明中使用inline。
String类
class String
{
public:
String(const char* cstr = 0);
String(const String& str); //参数是String类本身,所以叫拷贝构造函数
String& operator= (const String& str); //同样,拷贝赋值函数
~String();
char* get_c_str() const { return m_data; } //不改变,加const
private:
char* m_data;
}
构造函数和析构函数
inline
String:: String(const char* cstr = 0)
{
if (cstr)
{
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else
{
m_data = new char[1];
*m_data = '\0';
}
}
inline
String::~String()
{
delete[] m_data;
}
////////////////////////////////
String s1();
String s2("hello");
String* p = new String("hello");
delete p;
问题:new和新建对象方式有什么不同?
拷贝构造和拷贝赋值
带有指针的类必须有拷贝构造函数和拷贝赋值函数。为什么不带指针的类不需要呢?因为系统会默认给一个拷贝构造函数,使两边(二进制)内容一样。对于上面的complex类来说,正是我们需要的。
而对于带有指针的类来说,当使用默认拷贝(浅拷贝)的时候,两个对象指向同一个地方。假如有两个对象,String a(“hello”), String b(“World”). 当浅拷贝的时候,b的指针和a的指针相同,都指向hello的位置。此时world所占的内存就会被泄露。而此时如果a对这个指针指向的地方进行操作,b的值也会被改变。
所以,拷贝构造函数的作用:
- 防止内存泄露
- 使两个对象的操作互不影响。
这里主要是第二个影响吧,因为拷贝构造的对象还没有生成,所以没有开辟空间。第一个作用主要针对拷贝赋值函数。
为什么拷贝构造函数的参数是引用?
如果允许拷贝构造函数传值,就会在拷贝构造函数中调用拷贝构造函数,就会造成永无休止的递归调用从而导致栈溢出。
深拷贝:
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data , str.m_data);
}
////////////////
String s2(s1);
String s2 = s1;
拷贝构造和拷贝赋值的区别:拷贝构造是以b为蓝本创建一个a,拷贝赋值是使a和b的内容一样。a和b之前都已经被创造出来。
拷贝赋值:
inline
String& String:: operator = (const String& str)
{
if(this == &str) //检测自我赋值
{return *this;}
delete [] m_data;
m_data = new char[strlen(str.m_data)+1];
strcpy(m_data, str.m_data);
return *this;
}
步骤有3:
- 删除自己原来的内容。
- 开辟和对方一样的空间。
- 将对方的内容拷贝过来。
检测自我赋值的作用:
- 提高效率(如果是自我赋值的话,直接返回,不用进行下面的步骤)。
- 如果没有这个检测的话,在第一步删除了自身的内容,后面进行第二步的时候就会出错。
考虑上面代码的异常安全性的解法:
上面的代码先delete再new,如果这时内存不够,得到的就是一个空指针,容易引起程序崩溃。
解决的方案有两种:第一种,先new再delete掉原来的空间。这样如果new成功会释放原来的,new不成功能够保留原来的内容。第二种,创建临时实例。如下:
inline
String& String:: operator = (const String& str)
{
if(this!= &str)
{
String strTemp(str);
char* temp = strTemp.m_data;
strTemp.m_data = m_data;
m_data = temp;
}
return *this;
}
由于strTemp是一个临时变量,程序运行到if外面之后会自动调用析构函数。strTemp交换之后就是原来的内存空间,所以原来的空间被自动释放。
堆、栈和内存管理
栈,是存在于某作用域(scope)的一块内存空间。包括参数、返回地址、local object等。当离开这个作用域,对象的生命就会消失。
堆,是指由操作系统提供的一块global的内存空间,程序可以动态分配从中获得若干区块。
Complex c3(1,2);
{
Complex c1(1,2);
Complex* p = new Complex(3);
static Complex c2(1,2);
}
c2: 加上static关键字,c2的生命在作用域结束之后依然存在,直到整个程序结束。
问题:c2存放在哪里? 答:静态存储区
c3: global object, 作用域也是整个程序。和static有什么不同?存放在哪里?
static全局变量与普通全局变量有什么区别?
答:static全局变量和普通全局变量存储区域相同,不同的是:
static全局变量只在声明此static全局变量的文件中有效;
普通全局变量对整个源程序都有效,当此源程序包含多于一个文件的程序时,对其他文件依然有效。
new的过程
分为三个步骤:
分配内存
转型
构造函数
Complex *pc = new Complex(1,2); void* mem = operator new (sizeof(Complex)); pc = static_cast<Complex*>(mem); pc-> Complex::Complex(1,2);
operator new 内部调用malloc,申请一块空间。
将mem转成pc
通过指针调用构造函数,Complex(pc,1,2)。由于构造函数自带this指针,所以这里的this就是pc,而pc就是刚才申请的空间的地址。
delete的过程
String *ps = new String("Hello");
...
delete ps;
转化为:
String::~String(ps);
operator delete(ps);
- 调用析构函数,析构函数中delete[] m_data,hello存放的空间释放掉。
- 释放内存,调用free(ps),释放掉指针
类在内存中占用的空间
- Complex类,8个字节存放2个double数据,灰色的部分为调试信息。红色的部分是malloc和free为了回收记录的分配空间的大小。因为调用malloc只有一个指针,没办法确定大小。总共加起来空间是52,这个空间必须是16的倍数,所以调整到64.64转换成16进制是40,然后用1bit记录这块空间有没有被分配出去,所以变成了41,也就是红色部分的内容。
- 当不需要调试信息的时候,8+4*2,刚好是16
对于array new,一定要使用array delete
对于Complex类来说,比刚才多了一个3.用来记录数组的大小。当使用delete[] p
的时候,编译器就知道你要删除的是一个数组,因此会调用3次析构函数来释放空间。如果不加[],编译器只会调用一次,会发生内存泄漏。
调试信息什么时候被回收?是delete p的时候回收吗?
问题:第二步不是删除指针p吗,这里记录的是总的大小,为什么不直接删除所有的大小?
遇到的问题
错误 C4996 ‘strcpy’: This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details. MyString d:\program\c11\mystring\mystring\mysting.h 24
之前的strcpy函数被认为是不安全的,所以提供了strcpy_s代替。如果不想改的话,解决方法:
- 其实在下面的输出错误信息中有解决方法,“To disable deprecation , use _CRT_SECURE_NO_WARNINGS”,意思是我们可以不进行兼容性检查,我们可以在项目-属性-配置属性-c/c++-预处理器-预处理定义里边加上一句:_CRT_SECURE_NO_WARNINGS
- 也是在下面的输出信息中,我们可以看到有一处错误代号“ error C4996:”,所以我们可以在程序开头加上一句“#pragma warning(disable:4996)”就行,意思是忽略这个错误,
- 第三种方法是:我们可以在:项目-属性-配置属性-c/c++中的常规,里面有个SDL选项,关了。还有在代码生成中有个安全检查选项(/GS),关了。虽然这种方法也可以解决这个问题,但是我本人不太提倡这种解决办法,还是前两种解决方法比较好
补充
静态成员static
普通成员变量存在于每个对象当中,当调用成员函数的时候c1.real()
,默认传入第一个参数this指针,这样编译器就知道是谁调用了这个函数。
static成员变量指的是只有一份这个值,比如银行账户每个人都有,但是利率只有一个。
静态成员函数用来处理静态成员变量。
静态成员变量在类外定义
静态函数的方式:
- 类名调用Account::set_rate(5.0);
- 对象调用a.set_rate(7.0);
应用:单例模式
cout
在ostream中对cout做了各种类型的重载,所以cout可以输出很多种类型的数据。
类模板和函数模板
template<typename T>
对数据类型进行操作,每创造不同类型的对象,就会创造出一份代码。所以也有人说模板会引起代码的膨胀。
templete<class T>
inline
const T& min(const T&a, const T&b)
{
return b < a ? b : a;
}
当对一个类进行比大小的时候,发现对所有的类都是一样的操作,所以把类型变成一个类。
但是在使用的时候不用像类模板一样指明类型,编译器会进行自动推导。
当调用r3=min (r1,r2)的时候,b < a, 但是编译器并不知道怎么比较大小,所以去找b有没有对<的运算符重载。所以还应该在类里对<进行运算符重载。
namespace
这里const object和const member function相互调用的时候,当const对象调用非const函数时会报错。因为const object指定不会修改成员变量,但是成员函数却说我不保证成员变量不变。
当成员函数的const和non-const版本同时存在,const object只能调用const版本,non-const只能调用non-const。
在string类中有两个对[]的重载函数,const也是函数签名的一部分,所以这两个函数是同时存在的。
charT operator[] (size_type pos) const
{// 不用考虑COW(Copy On Write)}
reference operator[] (size_type pos)
{// 必须考虑COW}
C++学习笔记2中对于委托+复合中有一个例子,如上图。可以用多个指针指向一个string。但是对于这个string的调用会用一个reference counting 进行计数。如果对这个内容进行了改变,那么就要考虑COW.如果是常量字符串,那么就不可能改变内容,所以不必考虑COW。(由于上面的第一条规则,所以const成员函数只能被const对象调用。)
重载operator new/delete/[]
void* operator new(size_t size);
void operator delete(void* p,size_t size);
同样可以定义类成员的重载。
如果想要绕过自定义的重载函数,那么可以用::delete p;
对于new 和delete的重载:
- 可以重载operator new这个函数
- 可以重载成员或者全局函数
- 可以重载placement new(),但是必须有不同的参数列,第一个参数必须是size_t。也可以重载placement operator delete,但是不会被delete调用,只有当ctor出现异常,才会调用。 但是如果delete不一一对应new也没关系,只是自己放弃处理ctor发出的异常。
更多
operator type() const;
explicit complex;
pointer-like object;
template specialization;
standard library;
c++11;