c++学习-上

Static

  • 类外的 static 表示在链接时,当前变量只在当前翻译单元出现。
    • 例:在其他cpp定义 static变量a,不会与主函数的cpp(或另一个cpp文件)中出现的全局变量a(未加static)冲突。
      • 而如果未加static,主函数里的变量就必须加 extern 关键字,声明当前变量是调用其他cpp的。
  • 如果是类内的static变量,一个实例化对象改变了其值,其他所有对象的值都会改变。
    • 如果要调用类内的 static 变量需要提前声明。
      • 例:必须先以 C_robot::x 的方式声明 C_robot 类内的 static变量 x。
        • 通过上面方式声明后,C_robot 所有的实例化对象才可以调用 x。
          • 可以像普通变量一样定义,也可以用C_robot::x的形式直接赋值。

变量生存期和作用域

  • 生存期:变量实际存在的时间。
  • 作用域:变量访问的范围。
  • 栈的生命周期仅在函数或类局部;堆则是在全局。
    • 作用域结束,栈上的所有东西都会被释放(内存等)

静态局部变量

  • 变量生存周期是整个程序存在的时间。
  • 在函数内部用static定义的变量和在函数外用static定义的变量意义一样。(个人把这个也理解为全局变量)
  • 可以像下图这样在类内构造单例实例,以此来调用静态函数get(其生命周期是整个函数存在的时间)
    在这里插入图片描述

类的继承

  • 继承允许我们有一个相互关联的类的层次结构

多态

  • 一个单一类型,但拥有多个类型(子类拥有父类的一切public类型)

虚函数

  • 父类需要加virtual; 子类需要加 override
  • 虚函数需要两项额外的开销,一项是父类需要指向一个表;另一个在调用的时候需要额外的性能损耗。<但不会太大>

纯虚函数(接口)

  • 纯虚函数如果有子类继承就必须重写父类的函数,否则没法实例化

可见性

  • 友元可以访问私有属性(private)
  • protected 只有子类可以访问
  • 但上述两种都只能在类中访问,无法通过实例化对象访问
  • 通过public继承,不改变可变性
  • 通过private继承,所有成员都变为private
  • 通过protected 继承,所有成员都变为protected

数组

  • 数组实际上也是一个指针
  • 如果直接对一个指针加减,实际上是做一个指针的偏移,偏移的多少也取决于类型
  • 数组也可以通过new在堆上创建,如下例:
    • 但这种方式属于间接寻址,每次调用需要先找到实体位置,再找到数组的位置,因此尽量少用
main
{
int* a[6]; //在栈上创建
int* b = new int[6]; //在堆上创建
}
  • 由于数组一般是在栈上建立的,因此,没法直接获取其数组大小,应通过下述方式获得原始数组元素数量:
main
{
int a[6]; 
int count = sizeof(a) / sizeof(int);
}
  • 如果是C++11,可以直接用array来定义数组,获取大小可以直接使用.size函数:(但会因为边界检查,会有额外开销,所以开始原始数组快一些)
#include <array>
#include <iostream>
main
{
std::array<int,6> a; //创建数组
a.size();  //获取数组大小
}

字符串

注意:在字符串前加R代表忽略转义字符

  • 字符串可以理解为字符数组。
  • 双引号一般默认是字符串(const char*),在下例中,第一行指令是无法实现的,因为是const char*类型;但是可以通过第二行指令实现,因为这里的 += 是重载了的,所以可以这么用。
#include <array>
#include <string>
main
{
std::string a = "ljc" + "coder"; //报错
std::string a = std::string("ljc") + "coder"; //顺利执行
std::string a = "ljc" "coder"; //顺利执行
std::string a = "ljc";
a += "coder";                   //顺利执行
}
  • 为什么字符串会自动终止呢,实际上就是识别到0(不加引号的0)[终止标识符],就自动终止了。(\0)
  • C++中一般使用std::string模板类来表示字符串,其实际就是一个字符数组。
    • sting有一个构造函数可以接受 char* 或 const char* 类型的数据。
    • 如果要打印 string类型数据,必须加头文件 #define <string>,否则无法用cout输出打印string数据。
  • 复制字符串是比较耗时的,在函数变量如果直接用就会在栈上复制一个新的字符串,这是很不友好的,因此,对于只读的字符串我们通常使用 const +&的方式作为参数,如下例:
#include <array>
#include <string>
void example(std::string string) //不应该这么使用
void example(const std::string& string) //应该这么使用
  • 使用string时,我们在字符串前加一个大写的L,意在表示宽字符串;小写u表示char16;大写U表示char32
    • 一般两字节在windows上使用;四字节在linux(mac)上使用
#include <array>
#include <string>
#include<stdlib.h>
main
{

const char* name = "ljc"; //utf8
const wchar_t* name = L"ljc"; //2字节的字符
const char16_t* name = u"ljc"; //2字节16byte的字符 utf16
const char32_t* name = U"ljc"; //4字节32byte的字符 utf32
}

const

  • const int* 和 int const*代表的意义是一样的,关键在于 * 的位置。
  • 把const放在 * 后,变量名前: 让指针本身成为常量,不让指针分配其他指向。(但可以改变当前指针指向的内容)
  • 把const放在 * 前: 不能改变指向的内容,但可以让指针分配其他指向。
  • 在类中,放在方法名后面: 该类不能改变类的属性。(只能读取)
int* const a = new int; //不能改变指向,但可以改变指向的内容
const int* const a = new int; //不能改变指向,也不可以改变指向的内容
  • const 类只能调用 const 的方法
  • mutable函数允许const函数修改变量

mutable

  • <常用场景>
    使标记的变量可以在const的函数内使用(const类中的方法可以修改该变量)
  • <一般场景>
    修饰lambda表达式,值捕获时可以直接操作传入参数。(并非引用捕获,依旧值捕获,不修改原值)

成员初始化列表

  • 如下例所示,可以直接在类内(构造函数花括号上面)对参数初始化,这样在构造函数中就可以对函数名等进行初始化,使代码更易懂。
    • 但是要注意,初始化的顺序一定要和定义的顺序一致。
class Example
{
private :
   int m_core;
   std::string m_name; 
public:
   Example()
   :m_core(0),m_name("Unknown")
   {}
}
  • 无论如何应该使用成员初始化列表。
    • 一方面是代码简洁性。
    • 另一方面,其也会提高对代码的执行效率,如果不使用会造成性能的浪费。
      • 事实上我们如果在构造函数再建立一个构造函数,如果不用成员初始化列表,会生成两个对象(先调用一遍默认构造函数,这会造成性能的浪费)

三元操作符

if (m_level > 5)
m_level = 10;
else
m_level = 5;
//上式等价于下式三元操作符的
m_level > 5 ? 10 : 5

在堆、栈上创建C++实例化对象

  • 我们一般都是在栈上创建C++实例化对象,但也可以用关键字 new 在堆上创建。
    • 在堆上创建会耗时一些,并且需要我们手动将其内存删除。
    • 一般是对象太大或者希望显式的控制对象的生存期,会采用在堆上创建,否则一般都在栈上创建。
class Example
{}
main
{
   Example* e = new Example(); //堆上创建
   delete e; //用完后要记得delete掉new创建的内存
}

注:new实际和c中的malloc功能差不多

C++运算符和其重载

  • 下例是对加运算符重载的例子
    • 以及左移,<< 运算符的重载例子
class Example
{
float x, float y
Example (float x, float y)x(x),y(y) {}
Example ADD(const Example& other) const
{
return Example(x+other.x,y+other.y)}
Example ADD1(const Example& other) const
{
return operator+(other)//另一种写法
}
Example ADD2(const Example& other) const
{
return *this +other; //另一种写法
}
Example operator+(const Example& other) const //重载加运算符
{
return ADD(other)}
}
std::ostream& operator<<(std::ostream& stream, const Example& other) 
{
   stream<<other.x<<','<<other.y;
}
main()
{
   Example speed(1.1f,2.1f);
   Example position(0.5f,1.5f);
   Example result1 = position.ADD(speed);  // 普通
   Example result2 = position + speed; // 重载加运算符
   std::cout << result2 << std::endl; // 使用重载的<<运算符直接打印result2 的结果
}

this

  • this 是指向当前对象实例的指针,该方法属于对象实例

C++对象的生存期

  • 如下例所示,在花括号内实例化的e 对象实际上是在栈上建立的,花括号就是其作用域,在作用域结束后,会销毁栈上的所有东西,也就是说这时候就会调用类的析构函数。
    • 而e1是通过new关键字在堆上建立的,如果不delete,只有当应用程序整体结束才会自动清理其内存。
class Example
{}

main()
{

{ 
   Example e; //在栈上建立对象e
   Example *e1 = new Example();  // 在堆上建立对象e1
}

}

智能指针

uniqueptr(作用域指针)

  • 作用域指针可以在构造时用堆分配指针,在析构时删除指针。
    • 下例是自己构造的一个作用域指针
class Example
{}
class Scopedptr
{
private :
   Example* m_ptr;
public:
   Scopedptr(Example* ptr)
   	:m_ptr(ptr)
   	{}
   ~Scopedptr()
   {
   delete m_ptr;
   }
}
main()
{
Example* e = new Example(); // 没用作用域指针之前得这么在堆上建立对象,后面还需要自己delete
Scopedptr e = new Example(); // 使用作用域指针建立对象
Scopedptr e(new Example()); // 使用作用域指针建立对象的另一种写法
}
  • 沿用上例,可以直接调用unique_ptr的标准方法,出了花括号的作用域后,会自动析构当前对象(delete指针)
  • 但是unique_ptr创建的对象不能被其他指针指向,因为如果当前对象被自动析构了,第二个指针就会指向空,而share_ptr则不会,因为它是通过引用计数来跟踪指针有多少引用的。
class Example
{  }
main()
{
{
   std::unique_ptr<Example> e (new Example()) //一种使用作用域指针的方法(圆括号内是调用构造函数,因为unique_ptr是需要显式调用构造函数的),但是这种构造函数的调用方法并不安全
   std::unique_ptr<Example> e = std::make_unique<Example>(); // 一般使用这种方式调用构造函数(异常安全)
}
}

shareptr共享指针

  • 像上面说的,每次建立一次shareptr的指针,引用计数加一,而删除一个对应的指针后,引用计数减一,直到引用计数为零,彻底释放指针
  • 沿用上例
class Example
{  }
main()
{
{
   std::share_ptr<Example> e = std::make_shared<Example>();
   std::share_ptr<Example> e0 = e;
}
}
  • 或者像这里这样,创造两个作用域,在里面那个花括号里的作用域内有e,在出了里面这个花括号的作用域后,e0还是存在的。
class Example
{  }
main()
{
   std::share_ptr<Example> e0 ;
   {
   	std::share_ptr<Example> e = std::make_shared<Example>();	
   	e0 = e; 
   }
}
  • 注意:当你声明一个堆分配的对象,又不想自己清理,可以使用智能指针,并且优先使用 unique_ptr ,因为其具有一个较低的开销。
    • 但是,如果需要在对象之间共享时,使用share_ptr

拷贝和拷贝构造函数

  • 浅拷贝:用等号来赋值的语句,相当于只是复制了值,会占用两份内存
  • 深拷贝:根据定义复制整个对象。
    • 深拷贝出来clone之外,还可以通过拷贝构造函数完成。
  • 拷贝构造函数是一个构造函数,当你复制第二个字符串时,他会被调用。
    • 当你试图把字符串赋值给一个对象时,这个对象也是一个字符串。当你试图给他创建一个新的变量,并给他分配另一个变量时,这个变量和你正在创建的变量有相同的类型,而你复制这个变量,这就是拷贝构造函数。
class Example 
{}
Example* a = new Example();
Example*b = a;
b++; //这不会影响到a指针
b->3; //这会影响到a指针,因为他们指向同一个内存地址

  • 自己初始化一个string类
    • 在下例中会直接报错,因为在main函数中,实例化了两个对象,但他们实际上都是指向一个地址的,但在结束之后,会delete两次内存,所以会出现问题。
  • 下面的例子一定要仔细看,有注释很关键
class String
{
private:
	char* m_buffer;
	unsigned int m_size;
public:
	String(const char* string)
	{
		m_size = strlen(string);
		m_buffer = new char [m_size + 1]; //这里+1是给转义字符留一个位置
		memcpy(m_buffer,string,m_size);
		m_buffer[m_size] = 0; //手动增加空终止符 (\0)	
	}
	String(const String& other)//c++默认的拷贝构造函数,作用是将other浅拷贝给成员变量
		:m_buffer(other.m_buffer),m_size(other.m_size)
		{}
		//下面是深拷贝的拷贝构造函数
	String(const String& other)
		:m_size(other.m_size)
		{
			m_buffer = new char[m_size + 1];
			memcpy(m_buffer, other.m_buffer, m_size +1)
		}
		//或者将来拷贝构造函数写成下面这种
	String(const String& other)
	{
		memcpy(this,&other,sizeof(String))
	}
	//如果不要拷贝构造函数,可以使用下面的写法,禁止拷贝构造函数,事实上,unique_pyr也正是这么操作的
	String(const String& other) = delete
	~ String()
	{
	delete[] m_buffer;
	}
 	friend std::ostream& operator<<(std:ostream& stream,const String& string);
}
std::ostream& operator<<(std:ostream& stream,const String& string)
{
	stream << string.m_buffer;
	return stream
}
int main()
{
	String a = "abc";
	String b = "opq";
	std::cout << a << std::endl;
	std::cout << b << std::endl;
	
}
  • 上面我们可以看到,拷贝构造函数是在堆上创建的内存,而我们在函数调用时,很多时候不需要这样做,所以,可以在写参数时,用const引用对象的方式去传递对象,这样就不会调用拷贝构造函数,能很好的节省内存消耗。
  • 记住,我们常用const引用来传递对象
  • 例:
void printstr(const Example& e) //Example是上例中的一个类,当然也可以是标准类,如int、string等
{
	Example e1 = e;
}

箭头操作符

  • 例子:
    • 如果我们用指针调用对象的方法,使用箭头可以很好的帮我们简化代码
#include <iostream>
class Example
{
	void printf1();
}
main()
{
	Example a;
	*b = &a;
	b.printf1();  //会报错,因为 b 只是一个指针,不是对象,无法调用方法
	//可以通过下面的例子达到目的
	Example& c = *b;
	c.printf1();
	//但上面太冗余了,可以直接用箭头来表示
	b->printf1();
}

动态数组vector

  • 首先说一下为什么我用vector,而不是数组,一方面来说,vector可以动态扩展;另一方面,vector可以接受一个struct定义的类型结构体(比如一个坐标的x,y,z)为类型,这使代码更简洁,并且可以使函数的返回值可以是多个类型的。
  • 原理上来说,会先创建有10个元素的内存,如果你的输入超过了10个元素,会创建一个新的内存,把之前的元素复制过来,并删除原来的内存。
  • 总的来说标准模板库速度都会慢一些,如果需要速度,最好是自己重写一个模板库,当然,也可以通过使用emplace_back这种方式来优化。
  • 例子 (注意看里面的注释)
struct speed
{
	int x,y,z;
}
std::ostream operator<<(const ostream& stream, const speed& sp)
{
	stream << sp.x << "," << sp.y << "," << sp.z;
}

int main()
{
	std::vector<speed> sp ;
	sp.push_buck({1,2,3});
	sp.push_back({4,5,6});
	//遍历打印
	for (int i = 0, i < sp.size(), i++)
	{
		std::cout << sp[i] << std::endl;
	}
	for (const speed& s: sp)
	{
 		std::cout << s << std::endl;
	}
	//删除vector内某一项通过earse实现(借助迭代器)
	sp.erase(sp.begin()+1); //删除容器中的第二个元素
	//vector的优化使用方法
	//第一种:指定vector大小(这种方式仍然需要在main中构造再复制到vector中)
	sp.reserve(3) //创建大小为3的容器
	//第二种:(我们一般使用的方法)emplace_back
	sp.emplace_buck(1,2,3);//直接构造struct对象的参数传入vector而不需要复制
	sp.emplace_back(4,5,6);
	
	
	
}

struct

  • struct 往往被我们用于封装一个自己定义的类型空间
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值