Здравствуйте,
этот пост должен быть посвящен программированию на C++ в производственной среде. И это об автоматизации создания и проверки вашего кода. И эта статья разделена на 3 части: статический анализ, динамический анализ и гугл-тесты.

Давайте сделаем простое приложение для проверки концепции. Создайте класс MemoryTest с файлом memory_test.h

#include <iostream>
struct up_obj {
 const char * data;
};
class MemoryTest {
private:
 char * test;
protected:
public:
 MemoryTest();
 ~MemoryTest();
up_obj * upObj;
 void run();
};

простой файл memory_test.cpp

#include “memory_test.h”
#include “memory_test.h”
MemoryTest::MemoryTest() {
 this->test = (char*)malloc(10 * sizeof(char));
}
MemoryTest::~MemoryTest() {
// free(this->test);
}
void MemoryTest::run() {
 up_obj upObj;
 upObj.data = “hello world”;
this->upObj = &upObj;
 return;
}

и файл main.cpp

#include <iostream>
#include “memory_test.h”
int main() {
 MemoryTest memoryTest;
 memoryTest.run();
 char * mem = (char*)malloc(10 * sizeof(char));
 std::cout << memoryTest.upObj->data << “\n”;
 return 0;
}

Когда мы компилируем этот код и запускаем его, все работает нормально

$ ./cpp_production_ready
hello world

Так в чем проблема?

1 Статический анализ
статический анализ не является лекарством от всего, но является частью лучшего программирования. если вы используете компилятор llvm/clang для своего кода, я рекомендую вам использовать clang-static-analyzer, он создает хороший отчет, а также использовать clang с параметрами -fsanitize=address -fno-omit-frame-pointer для обеззараживания адресов утечек. Подробнее об этом можно прочитать здесь. Но мы хотели бы поговорить о другом программном обеспечении: cppcheck. Cppcheck имеет множество плагинов для разных IDE (CLion, QtCreator и т. д.).

$ cppcheck --enable=warning,performance,portability,style ../*.cpp
Checking ../main.cpp …
[../main.cpp:7]: (style) Variable ‘mem’ is assigned a value that is never used.
[../main.cpp:11]: (error) Memory leak: mem
1/2 files checked 44% done
Checking ../memory_test.cpp …
[../memory_test.cpp:3]: (warning) Member variable ‘MemoryTest::upObj’ is not initialized in the constructor.
[../memory_test.h:10]: (style) class ‘MemoryTest’ does not have a copy constructor which is recommended since the class contains a pointer to allocated memory.
[../memory_test.h:12]: (style) Class ‘MemoryTest’ is unsafe, ‘MemoryTest::test’ can leak by wrong usage.
2/2 files checked 100% done

Мы видим никогда не использовавшееся значение mem в строке 7 файла main.cpp. Также мы видим утечку памяти переменной mem в строке 11, потому что эта память не освобождается. Cppcheck обнаружил некоторые переменные, которые не инициализированы в конструкторе класса, но это просто предупреждение, это не должно быть проблемой.

Но cppcheck не обнаружил ошибку use-after-free. Потому что upObj в строке 12 в memory_test.cpp является локальной переменной и исчезает после возврата из функции. Что дальше? Исправьте ошибки, найденные с помощью статического анализа, и перейдите к динамическому анализу.

2 Динамический анализ
мы используем valgrind для динамического анализа, но существует множество инструментов для этой работы, вы можете найти их здесь. Хорошо, запустим.

$ valgrind --leak-check=full --track-origins=yes ./cpp_production_ready
==21434== Memcheck, a memory error detector
==21434== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==21434== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==21434== Command: ./cpp_production_ready
==21434==
==21434== Conditional jump or move depends on uninitialised value(s)
==21434==    at 0x4F49F3C: std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) (ostream:558)
==21434==    by 0x400AEA: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==  Uninitialised value was created by a stack allocation
==21434==    at 0x400AD7: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==
==21434== Use of uninitialised value of size 8
==21434==    at 0x4C2DBA2: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==21434==    by 0x4F49F48: length (char_traits.h:267)
==21434==    by 0x4F49F48: std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) (ostream:562)
==21434==    by 0x400AEA: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==  Uninitialised value was created by a stack allocation
==21434==    at 0x400AD7: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==
==21434== Use of uninitialised value of size 8
==21434==    at 0x4C2DBB4: strlen (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==21434==    by 0x4F49F48: length (char_traits.h:267)
==21434==    by 0x4F49F48: std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) (ostream:562)
==21434==    by 0x400AEA: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==  Uninitialised value was created by a stack allocation
==21434==    at 0x400AD7: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==
==21434== Use of uninitialised value of size 8
==21434==    at 0x574EC5E: _IO_default_xsputn (in /usr/lib/libc-2.24.so)
==21434==    by 0x574CF46: _IO_file_xsputn@@GLIBC_2.2.5 (in /usr/lib/libc-2.24.so)
==21434==    by 0x57420FA: fwrite (in /usr/lib/libc-2.24.so)
==21434==    by 0x4F49BF5: sputn (streambuf:451)
==21434==    by 0x4F49BF5: __ostream_write<char, std::char_traits<char> > (ostream_insert.h:50)
==21434==    by 0x4F49BF5: std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) (ostream_insert.h:101)
==21434==    by 0x4F49F56: std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) (ostream:561)
==21434==    by 0x400AEA: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==  Uninitialised value was created by a stack allocation
==21434==    at 0x400AD7: main (in /home/burso/Projects/medium_blog/cpp_production_ready/.build/cpp_production_ready)
==21434==
hello world
==21434==
==21434== HEAP SUMMARY:
==21434==     in use at exit: 0 bytes in 0 blocks
==21434==   total heap usage: 2 allocs, 2 frees, 73,728 bytes allocated
==21434==
==21434== All heap blocks were freed -- no leaks are possible
==21434==
==21434== For counts of detected and suppressed errors, rerun with: -v
==21434== ERROR SUMMARY: 14 errors from 4 contexts (suppressed: 0 from 0)

Теперь вы можете увидеть проблему неинициализированного значения, потому что оно было создано выделением стека. Так почему же он все еще показывает «hello world», когда мы запускаем приложение? Потому что память стека не перезаписывалась, а просто освобождалась. Это означает, что можно показывать «hello world», пока память не перезаписывается, но это не гарантируется.

3 Google Test
Когда мы уже исправили все эти ошибки, для чего нам нужен google-test?
Потому что нам также нужно знать, возвращают ли функции правильные значения. Давайте представим, что мы создаем какую-то библиотеку или API и после релиза вам нужно что-то изменить. Google Test предлагает вам проверить ваше изменение. Если, например, API возвращает все те же предполагаемые правильные значения.

У меня сейчас нет примера для Google Test, но вы можете найти его в документах.

наконец, мы можем написать автоматический скрипт, который строит и проверяет что-нибудь для нас, как этот

#!/bin/bash
PROJECT_DIR=$(pwd)
BUILD_DIR=”$PROJECT_DIR/.build”
BINARY_NAME=”cpp_production_ready”
EXIT_CODE=5
LOG_FILE=”$PROJECT_DIR/build.log”
TEMP_LOG_FILE=”$PROJECT_DIR/build.log.tmp”
mkdir -p $BUILD_DIR
cd $BUILD_DIR && cmake -DCMAKE_BUILD_TYPE=Debug $PROJECT_DIR
make -j4
# STATIC ANALYSIS
cppcheck — error-exitcode=$EXIT_CODE — enable=warning,performance,portability,style $PROJECT_DIR/*.cpp >> $TEMP_LOG_FILE 2>&1
if [ “$?” -eq “$EXIT_CODE” ]; then
 echo “== STATUS: static analysis error (check $LOG_FILE)”
fi
# DYNAMIC ANALYSIS
valgrind — error-exitcode=$EXIT_CODE — leak-check=full — track-origins=yes $BUILD_DIR/$BINARY_NAME >> $TEMP_LOG_FILE 2>&1
if [ “$?” -eq “$EXIT_CODE” ]; then
 echo “== STATUS: dynamic analysis error (check $LOG_FILE)”
fi
# GENERATE LOG FILE
mv $TEMP_LOG_FILE $LOG_FILE
# DELETE BUILD DIRECTORY
rm -rf $BUILD_DIR

и вывод build.sh

$ ./build.sh
 — The C compiler identification is GNU 6.3.1
 — The CXX compiler identification is GNU 6.3.1
 — Check for working C compiler: /usr/bin/cc
 — Check for working C compiler: /usr/bin/cc — works
 — Detecting C compiler ABI info
 — Detecting C compiler ABI info — done
 — Detecting C compile features
 — Detecting C compile features — done
 — Check for working CXX compiler: /usr/bin/c++
 — Check for working CXX compiler: /usr/bin/c++ — works
 — Detecting CXX compiler ABI info
 — Detecting CXX compiler ABI info — done
 — Detecting CXX compile features
 — Detecting CXX compile features — done
 — Configuring done
 — Generating done
 — Build files have been written to: /home/burso/Projects/medium_blog/cpp_production_ready/.build
Scanning dependencies of target cpp_production_ready
[ 33%] Building CXX obje.BasicString__Test_CorrectReturnValuect CMakeFiles/cpp_production_ready.dir/main.cpp.o
[ 66%] Building CXX object CMakeFiles/cpp_production_ready.dir/memory_test.cpp.o
[100%] Linking CXX executable cpp_production_ready
[100%] Built target cpp_production_ready
== STATUS: static analysis error (check cpp_production_ready/build.log)
== STATUS: dynamic analysis error (check cpp_production_ready/build.log)

вы можете найти весь код здесь. Спасибо, что прочитали этот текст, и если у вас возникнут какие-либо вопросы, не стесняйтесь писать мне.