Android JNI 编程 - C++基础 (四)

725 阅读3分钟

c和c++ 几个关键不同

这一小节 主要讲述一下,c语言和c++语言 几个关键的不同,体会一下c++ 相比与c语言 做了哪些改进。 有了c的基础 学习c++ 会快不少

c语言中的字符问题

image.png

c2报错,应该很容易理解了, 字符 只能和 '' 匹配 slash2 报错,因为c语言中字符串必须是 '\0' 结尾 ,而单引号显然没有 \0 这个东西,

所以你看c语言中你要写个字符串和字符是真得小心。

c++ 中有了string 以后 就方便很多了

char c1 = 'yes';
cout << c1 << endl;
string s1(1, 'yes');
cout << s1 << endl;
string s2(3, 'yes');
cout << s2 << endl;
string s3("yes");
cout << s3 << endl;

看下输出

image.png

我们终于不用关心到底应该用''还是""了。

数组退化问题

先看一下 下面的代码:


#include <iostream>
using namespace std;

double average1(int arr[5]) {
  double result = 0;
  int length = sizeof(arr) / sizeof(arr[0]);
  cout << "average1-length:" << length << endl;
  for (int i = 0; i < length; ++i) {
    result += arr[i];
  }
  return result / length;
}

int main() {

  int array[] = {1, 2, 3, 4, 5};
  // 求数组长度的技巧
  int length = sizeof(array) / sizeof(array[0]);
  cout << "length:" << length << endl;
  cout << "average1:" << average1(array) << endl;

  return 0;
}

一个很简单的求平均值的程序

image.png

看下执行结果,可能不熟悉c语言的人都懵逼了, 为啥函数内部的数组的长度计算出来的值是2?

如果你用新版本的ide的话会看到提示:

image.png

讲白了,对于c语言来说, 数组作为参数来传递的时候, 实际上这个函数的参数 就变成了 数组的指针

对于64位的电脑来说,数组的指针就是大小就是8,而一个int值大小是4 那自然算出来就是2了。

也就是说 上面的函数等同于

double average1(int* arr) {
  double result = 0;
  int length = sizeof(arr) / sizeof(arr[0]);
  cout << "average1-length:" << length << endl;
  for (int i = 0; i < length; ++i) {
    result += arr[i];
  }
  return result / length;
}

在C语言中,数组名常常被解释为指向数组第一个元素的指针。由于C语言中指针是一种特殊的变量类型,所以数组名也可以被视为指向一个特定类型的指针。

然而,当我们将一个数组作为参数传递给函数时,数组名就会发生一种奇怪的“退化”现象,即它不再是指向数组的指针,而是成为了一个普通的指针。

这种现象的原因是当数组作为函数参数时,它会自动转换为指向其第一个元素的指针。因此,在函数中使用数组名时,它实际上是一个指向数组第一个元素的指针,而不是一个真正的数组。

这种“退化”现象可能会导致一些问题,特别是当我们试图确定数组的长度时。因为在函数中,我们只能看到指针,而不能看到指针所指向的数组的大小。

因此,如果我们想在函数中使用一个数组,并需要知道其大小,我们必须将数组的大小作为另一个参数传递给函数。

c语言这么做主要是为了效率考虑,毕竟如果是值传递的话 涉及到数组的拷贝 那效率就太低了

对于c语言来说 可以这么改(其实就是让调用者把数组的实际长度传递进去就可以了):

double average2(int* arr,int length) {
  double result = 0;
  cout << "average1-length:" << length << endl;
  for (int i = 0; i < length; ++i) {
    result += arr[i];
  }
  return result / length;
}

c++ 怎么解决这个问题的?

首先改一下cmake文件

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")

然后用stl库的方法来解决就行了

vector<int> myArray {1, 2, 3, 4, 5};
cout << "average3:" << average3(myArray) << endl;
double average3(vector<int> &v) {
  double result = 0;
  auto it = v.begin();
  for (; it != v.end(); ++it) {
    result += *it;
  }
  return result / v.size();
}

考虑一个问题,如何实现一个动态长度的数组?对于c语言来说,必然要用到malloc和free,很麻烦,而在c++中我们很容易用vector就可以实现了 这里就不继续写了,大家有个印象即可

算术右移和逻辑右移

先看一段代码:

int main() {
  char a1=0x63;//0110 0011
  a1=(a1<<4); //0011 0000
  printf("0x%x \n",a1);


   a1=0x63;//0110 0011
  a1=(a1>>4); //0000 0110
  printf("0x%x \n",a1);


  char a2=0x95;//1001 0101
  a2=(a2<<4); //0101 0000
  printf("0x%x \n",a2);


  a2=0x95;// 1001 0101
  a2=(a2>>4); // 以为是0000 1001 结果实际是 1111 1001 算数右移 逻辑右移?
  printf("0x%x \n",a2); //为啥是0xfffffff9 ?



  return 0;
}

有问题的程序实际在这:

image.png

这个和我们想象中的 0000 1001的结果并不一样

实际上在c语言中

右移操作符分为算术右移和逻辑右移。

逻辑移位:左边用0填充,右边丢弃

算术移位:左边用原该值的符号位填充,右边丢弃

但常见的编译器是算数右移

要解决上面那个问题 用 无符号数来表示即可

unsigned char a2=0x95;//1001 0101

看下结果:

image.png

所以这里c语言就有带点坑了,对于c语言来说,我们使用位移运算符的时候 要考虑上下文的环境 到底是算数右移动还是逻辑右移, 到底要不要用无符号数

c++的解决方案 bitset

int main() {
  bitset<8> bt=0x95;
  cout<<bt<<endl;
  bt=(bt>>4);
  cout<<bt<<endl;

  cout<<"--------------------"<<endl;


  //c++ 就简单的多,用bitset言简意赅 实现一个简单的权限判断
  bitset<10> priv=0xFF;
  bitset<10> P_BACKUP=(1<<6);
  bitset<10> P_ADMIN=(1<<7);
  cout<<priv<<endl;
  cout<<P_BACKUP<<endl;
  cout<<P_ADMIN<<endl;

  if ((priv & P_BACKUP)==P_BACKUP){
    cout<<"backup"<<endl;
  }
  if ((priv & P_ADMIN)==P_ADMIN){
    cout<<"admin"<<endl;
  }
  return 0;
}

看下执行结果:

image.png

强制类型转换

看下面这个简单的代码:

int main() {
  int array[] = {1, 2, 3};
  cout << sizeof(array) / sizeof(array[0]) << endl;
  int threshold = -1;
  if (sizeof(array) / sizeof(array[0]) > threshold) {
    cout << "合法数组" << endl;
  } else {
    cout << "非法数组" << endl;
  }

  return 0;
}

应该每个人看到了 都觉得这个地方肯定会打印合法数组 对吧

但实际上跑一下你就知道

image.png

竟然是非法的? 这是为啥?

其实是因为 sizeof 的返回值是一个无符号类型的, 当你用上述的写法的时候 threshold这个值-1 会自动转换为 有符号类型的,

cout << (unsigned int) threshold<< endl;

这个打印一下出来的值 是 4294967295,真相大白了

所以这个地方非常坑,解决方案, 把 无符号转成有符号 ,然后再比较即可

int array[] = {1, 2, 3};
cout << sizeof(array) / sizeof(array[0]) << endl;
int threshold = -1;
int res = sizeof(array) / sizeof(array[0]);
if (res > threshold) {
  cout << "合法数组" << endl;
} else {
  cout << "非法数组" << endl;
}

c语言中的强制类型转换 隐藏的比较深,很容易出错,而且这种错很难排查 ,因为你很难一眼看出来 我到底哪里做了类型转换?

c++ 怎么解决的?

c++ 其实就是用了一些新的东西 xxx_cast 的关键字 来显示的告诉编程者 这里做了类型转换,转换成了哪种类型

例如:

double a= static_cast<double>(1);

在c++中,处理大数的运算 一般都是用boost库,大家了解一下即可。

c语言中的字符串缺陷

int main() {
  char str1[] = "string";
  cout << strlen(str1) << endl; //6
  cout << sizeof(str1) / sizeof(str1[0]) << endl;//7


  char str2[]="stri\0ng";
  cout << strlen(str2) << endl; //4
  cout << sizeof(str2) / sizeof(str2[0]) << endl;//8

  return 0;
}

输出如下:

image.png

这个输出结果不用解释了吧,讲白了,strlen计算长度是计算的 \0 之前的长度,

char str1A[30]="stringA";
// strcat 这个函数的第一个参数的长度 一定要足够容纳 拼接后的长度
strcat(str1A,str2);
cout << str1A << endl; //stringAstri
cout << strlen(str1A) << endl; //11
cout << sizeof(str1A) / sizeof(str1A[0]) << endl;//30

c++中的解决方案

string sstr1="string";
cout << sstr1 << endl; //stringAstri
cout << sstr1.length() << endl; //6
cout << sizeof(sstr1) << endl; //8
cout << sstr1.capacity() << endl; //6
cout << sstr1.size() << endl; //6


string sstr2="stri\0ng";
cout << sstr2 << endl; //stringAstri
cout << sstr2.length() << endl; //4
cout << sizeof(sstr2) << endl; //8
cout << sstr2.capacity() << endl; //4
cout << sstr2.size() << endl; //4

sstr1 += sstr2;
cout << sstr1 << endl; //stringAstri
cout << sstr1.length() << endl; //10
cout << sizeof(sstr1) << endl; //8
cout << sstr1.capacity() << endl; //12
cout << sstr1.size() << endl; //10

c++中 字符串的写法 比c语言中要方便不少, 但是要注意几点,length返回的依然是字符串实际的长度,capactity是实际容量的大小,跟扩容相关

这里提一下,有兴趣的可以看下redis的字符串实现,效率上比这里原生的要高不少。

字符串的不可变性

int main() {
  char array[]="helloworld";
  array[2]='c';
  cout<<array[2]<<endl;


  char* str="helloworld";
   str[2]='c'; //interrupted by signal 11: SIGSEGV)
   cout<<str<<endl;

  return 0;
}

上述代码中的第二段 执行起来会报错, 这里就涉及到一个字符串的不可变性问题了,其实和java是有相似性的,大家自行体会一下即可。 不要犯错就行

C++中的指针

容易混淆的数组的指针和指针的数组

int main() {
  // T* t[] 指针的数组   int* a[] 里面的值 都是指针
  // T (*t)[] 数组的指针 int (*a)[]  []优先级比较高

  int c[3] = {1, 2, 3};
  int *a[3]; // a代表一个长度为3的数组,元素是指针
  int (*b)[3]; // 代表一个数组长度为3的数组的指针
  b = &c; // b指向c
  // 下面是将c中的每一个元素的地址赋值给a
  for (int i = 0; i < 3; ++i) {
    a[i] = &c[i];
  }

  cout << "a[0]:" << *(a[0]) <<"-------"<< a[0] << endl;
  cout << (*b)[0] <<"------------"<< &((*b)[0]) << endl;

  return 0;
}

image.png

const 与指针

image.png

const 在 最左边也是一样的规则

image.png

指向指针的指针

int main() {
  int a=123;
  // *操作符具有从右向左的结合性
  int* b=&a;
  // *(*c)
  int** c=&b;



  return 0;
}

野指针

这里要注意了,多数java程序员 刚开始写c的时候 很容易犯下面这个错误

image.png

原因就在于,很多javaer 会误以为 这里执行的时候会报错,至少是个空指针之类的

然而并没有

这段程序编译是ok的,甚至运行起来也是ok的( 严格来说运行是否ok 要看运气)

这里本质上定义了一个int 类型的指针, 但是你没有给这个指针赋予一个地址

在没有地址的情况下,你给他写入了一个值。

这个就很危险了,因为运行的时候 鬼才知道a的地址会被随机分配到哪里。。。

所以千万不要这么写!!!!!

cpp中是需要你手动给一个指针设置null的

a= NULL;

手动设置为null以后 运行起来就会出错了

image.png

这里跟java 是不同的,java你 定义一个String a,默认就指向null

**另外要注意的是 delete和free 之后的指针 一定要记得设置为null,否则很容易成为野指针 **

**没有初始化的,不用的 或者超出范围的指针 一定要设置为null **

这里最难的就是判断一个指针的作用范围,只能靠多写 多找感觉

内存泄漏

c++ 没有什么gc的概念,需要你自己手动管理堆区的内存

int main() {
  int *c = new int(1000);
  if (c != NULL) {
    delete c;
    c = NULL;
  }
  int* arr=new int[1000];
  if (arr != NULL) {
    // 新手比较容易遗忘 数组也需要释放 数组的释放要加[]
    delete[] arr;
    arr = NULL;
  }

  // 其他对象的delete就不演示了


  return 0;
}

智能指针 auto_ptr

这个东西在高版本的c++代码中 会被移除, 原因是 他有个坑

假设你用ap1 去指向一块内存区域b 另外一个人用ap2 也指向内存区域b 当ap2 指向内存区域b的时候,ap1 自动就指向null了。。。。

这和java的很不一样吧。

使用这货 一定要注意

int main() {
  // 对int 使用  不需要你delete操作,在离开作用域时自动释放
  auto_ptr<int> p(new int(10));
  cout << *p << endl;

  // 对数组使用

  auto_ptr<string> lan[5]={
      auto_ptr<string>(new string("C++")),
    auto_ptr<string>(new string("Java")),
    auto_ptr<string>(new string("C#")),
    auto_ptr<string>(new string("Python")),
    auto_ptr<string>(new string("Ruby"))
  };

  auto_ptr<string> pC;
  // 这里就有坑了,你不能这样做,因为你的pC指向的是lan[1]的内存,而lan[1]的内存是被释放的
  pC = lan[1];
  // 这里是ok的 会打印java
  cout << *pC << endl;

  for (int i = 0; i < 5; ++i) {
    // 但是这里就坑了,当i=1的时候 程序就会报错了
    cout << *lan[i] << endl;
  }
  
  return 0;
}

智能指针 unique_ptr

他只能被一个对象持有,不支持复制和赋值

虽然不能拷贝,但是可以支持 转移所有权。

下面看个例子

这里显然就会报错了:

image.png

转移所有权就是可以的:

int main() {

  auto w = make_unique<int>(10);
  cout << *(w.get()) << endl;

  auto w2 =std::move(w);
  cout << *(w2.get()) << endl;
  cout << *(w.get()) << endl; // 这行会报错,因为w已经被移动了


  return 0;
}

智能指针 shared_ptr

这个跟java中就有一点点像了

是通过引用计数共享一个对象

当引用计数为0时,这个对象没有被使用,就可以进行析构了。 是不是很像java中的引用计数法?

同时呢,循环引用也会在这里出现了。这里的概念 javaer 应该都很熟悉了

同时,还有一个weak_ptr 与这个shared_ptr 共同工作。 也很像java中的弱引用

来看几个例子

int main() {

  {
    auto wA = shared_ptr<int>(new int(20));
    {
      auto wA2 = wA;
      cout << *wA2.get() << endl;//20
      cout << *wA.get() << endl;//20

      cout << wA2.use_count() << endl;//2
      cout << wA.use_count() << endl;//2
    }
    // 下面这个输出1 是因为 wA2 已经出了大括号了,作用域结束了,所以 wA 的引用计数为1
    cout << wA.use_count() << endl;//1

  }

  return 0;
}

再看一个例子

#include <iostream>
#include <memory>
using namespace std;

// 声明
struct B;
struct A {
  shared_ptr<B> pb;
  // 析构函数, 用来说明被释放掉
  ~A() {
    cout << "A::~A()" << endl;
  }
};

struct B {
  shared_ptr<A> pa;
  ~B() {
    cout << "B::~B()" << endl;
  }
};

struct BW;
struct AW {
  shared_ptr<BW> pb;
  // 析构函数, 用来说明被释放掉
  ~AW() {
    cout << "AW::~AW()" << endl;
  }
};

struct BW {
  weak_ptr<AW> pa;
  ~BW() {
    cout << "BW::~BW()" << endl;
  }
};

void Test() {
  shared_ptr<A> tA(new A());
  shared_ptr<B> tB(new B());
  cout << "tA.use_count():" << tA.use_count() << endl;
  cout << "tB.use_count():" << tB.use_count() << endl;
  tA->pb = tB;
  tB->pa = tA;

  cout << "tA.use_count():" << tA.use_count() << endl;
  cout << "tB.use_count():" << tB.use_count() << endl;
}


void Test2() {
  shared_ptr<AW> tAw(new AW());
  shared_ptr<BW> tBw(new BW());
  cout << "tAw.use_count():" << tAw.use_count() << endl;
  cout << "tBw.use_count():" << tBw.use_count() << endl;
  tAw->pb = tBw;
  tBw->pa = tAw;

  cout << "tAw.use_count():" << tAw.use_count() << endl;
  cout << "tBw.use_count():" << tBw.use_count() << endl;
}

int main() {

 Test();
 Test2();

  return 0;
}

看下输出:

image.png

这里要注意2点,

tAw的 引用计数是1的原因是 tbW是以weak的形式去持有他的

第二个要注意的就是 Test函数发生了内存泄漏 因为没有走 析构函数

引用

引用在c++中就是一个 不允许修改的指针

对我这种新手来说,能用引用就不要用指针了,指针稍微不注意 就容易埋坑

引用可以认为是指定变量的别名,使用时可以认为是变量本身

引用的使用比较简单 这里就不展开讲了。